SPB Git

Repository Public

codex-profile.md 11.7 KB

# Codex profile ("Codex Remote")

The same code base runs two products. An instance serves exactly one agent profile, selected by CR_AGENT (read by apps/runtime and apps/server; the web build is shared and learns the profile at run time):

CR_AGENT=claude (default, unset) CR_AGENT=codex
Product / agent name Claude Remote / Claude Code Codex Remote / Codex
Binary CR_AGENT_BIN, else CR_CLAUDE_BIN, else claude CR_AGENT_BIN, else codex (CR_CLAUDE_BIN ignored)
Default tmux socket (CR_TMUX_SOCKET unset) claude-remote codex-remote
Default data dir (CR_DATA_DIR unset) ~/claude-remote-data ~/codex-remote-data
Agent session command `$SHELL -lc 'exec claude [--continue --resume ]'`
Conversation id rule ^[A-Za-z0-9][A-Za-z0-9-]{7,63}$ UUID only
WAITING_FOR_INPUT signal hooks Notification, Stop (settings.json) notify program → agent-turn-complete → Stop
Back to RUNNING hook UserPromptSubmit first terminal input from a browser
Hook installation CR_INSTALL_AGENT_HOOKS=1 or CR_INSTALL_CLAUDE_HOOKS=1 CR_INSTALL_AGENT_HOOKS=1 only

The definition is packages/shared/src/agent.ts (AGENT_PROFILES, capability flags, Branding). With no new variable set, every default is the previous Claude behaviour.

# Session kind: no new value, no migration

SessionKind stays 'claude' | 'shell' on the wire, in RtSessionSpec and in the database (terminal_sessions.kind, CHECK kind IN ('claude','shell')). 'claude' now means "the instance's agent": Claude Code on a Claude instance, Codex on a Codex instance. Reasons: the live database has rows with kind='claude', each instance has its own database so the value is never ambiguous, and no migration or constraint change is needed. Only labels differ in the UI. The column claude_conversation_id likewise holds the Codex session id on a Codex instance.

# Verified facts about the Codex CLI (codex-cli 0.166.0, macOS arm64)

Checked on the development laptop with the real binary and a temporary CODEX_HOME; no model call was made.

  • CODEX_HOME is honoured for configuration, authentication, sessions and MCP (codex login status answers "Not logged in" in an empty one while the real home is logged in).
  • codex --version → codex-cli 0.166.0.
  • codex login status prints on stderr: Logged in using ChatGPT (exit 0) / Not logged in (exit 1). The runtime reads stdout + stderr and keeps only a boolean and a label (chatgpt, api_key); the rest of the line (a masked key for API-key logins) is never returned.
  • Resume: codex resume [SESSION_ID] ("Session id (UUID) or session name"), codex resume --last ("Continue the most recent session without showing the picker"; filtered by cwd unless --all). A free positional argument of codex / codex resume is a prompt that runs immediately.
  • Approval / sandbox bypass flags present in codex --help: --dangerously-bypass-approvals-and-sandbox, --dangerously-bypass-hook-trust, --approve-for-me, -s/--sandbox danger-full-access, -a/--ask-for-approval never. Hidden aliases accepted by the parser (tested with --help): --yolo, --not-so-yolo. --full-auto does not exist in this version (still refused by name). Configuration overrides: -c/--config key=value, --enable, --disable, -p/--profile.
  • Sessions ("rollouts"): $CODEX_HOME/sessions/YYYY/MM/DD/rollout-<local time>-<uuid>.jsonl. Line 1: {"type":"session_meta","payload":{"id":<uuid v7>,"cwd":<abs path>,"originator":"codex-tui", "thread_source":"user","source":…,"base_instructions":…}} (≈ 22 kB). User prompts are response_item records with payload.type="message", role="user", content:[{"type":"input_text","text":…}]; the first one is an injected <environment_context> block.
  • MCP: codex mcp list --json (array; transport.type stdio | streamable_http; env contains the values in clear; auth_status unsupported | not_logged_in | bearer_token | o_auth), codex mcp add <name> [--env K=V]… -- <command> [args…], codex mcp add <name> --url <url> [--bearer-token-env-var VAR], codex mcp get <name> [--json] (unknown name: exit 1, Error: No MCP server named '<name>' found.), codex mcp remove <name> (unknown name: exit 0, No MCP server named '<name>' found.). add on an existing name replaces it silently. One scope (global, config.toml), no SSE, no custom headers from the CLI, no health check in list.
  • notify: top-level key of config.toml, an argv array; the binary contains the legacy-notify payload fields type (agent-turn-complete), thread-id, turn-id, cwd, client, input-messages, last-assistant-message. Codex Desktop's "Computer Use" plugin sets this key itself (notify = ["…/SkyComputerUseClient", "turn-ended"] on the laptop).
  • A hooks mechanism also exists (codex features list: hooks stable true; events in the binary include UserPromptSubmit, Stop, SessionStart, PreToolUse, …, configured in hooks.json), but hooks need a persisted trust decision made interactively (hence --dangerously-bypass-hook-trust). It is not used: installing it unattended would mean bypassing that trust.
  • Skills: $CODEX_HOME/skills/<name>/SKILL.md, bundled ones in $CODEX_HOME/skills/.system/; the binary also references .agents/skills and .codex/skills. Instructions: AGENTS.md, AGENTS.override.md. No $CODEX_HOME/prompts custom-prompt directory is referenced by this version.

# What the runtime does differently (apps/runtime)

  • Launch (codex-args.ts, sessions.ts). Accepted argument lists: none, resume --last, resume <uuid>. Refused with 400 forbidden_flag: every bypass flag above (any spelling containing bypass or yolo), -c/--config, --enable, --disable, -p/--profile, --remote. Refused with 400 invalid_resume: any other shape — in particular a free positional argument.
  • GET /health gains agent and agentHooks: {requested, status: not_requested | installed | conflict | error, detail}.
  • GET /claude/info: codex --version + codex login status; configDir = $CODEX_HOME; agent: "codex".
  • POST /claude/conversations: rollouts whose recorded cwd equals the requested directory (real or lexical path), newest first by mtime, at most 50. Bounded: 400 newest files examined, 256 KiB read per file for the meta record, 4 MiB per listed session. Sub-agent threads (thread_source ≠ user) and source: "exec" runs are skipped.
  • GET /skills: kind skill from $CODEX_HOME/skills, ~/.agents/skills, $CODEX_HOME/skills/.system (source plugin = bundled with the CLI), and for a project <root>/.agents/skills, <root>/.codex/skills; kind agent for AGENTS.md / AGENTS.override.md (global and project). Never kind command. POST /skills/op works on skills only (user scope $CODEX_HOME/skills, disabled ones parked in $CODEX_HOME/.disabled/skills; project scope <root>/.agents/).
  • /mcp: list through codex mcp list --json (scope always user, status unknown, or needs_auth when auth_status is not_logged_in; "Disabled" as lastError); env values, URL credentials and query strings are dropped. add refuses SSE, headers and project scope (400 unsupported) and an existing name (409, checked first because the CLI would overwrite). test = codex mcp get --json: it only proves the server is configured.
  • Notify (codex-notify.ts, bin/cr-codex-notify). With CR_INSTALL_AGENT_HOOKS=1 the runtime sets notify = ["<repo>/apps/runtime/bin/cr-codex-notify"] in $CODEX_HOME/config.toml by a line-based edit of the top-level key (everything else is preserved byte for byte; file mode kept; atomic write). An existing notify that is not ours is never replaced: the file is left untouched, /health reports agentHooks.status = "conflict" and /system/status shows a DEGRADED agent-hooks component. The script is a no-op outside a Codex Remote session; inside it forwards the JSON (stdin of curl, token on fd 3) to POST /hooks/claude, where agent-turn-complete becomes Stop with thread-id as conversation id. The prompt and the assistant message contained in the payload are not kept.

# What the server does differently (apps/server)

  • GET /auth/status → branding: {agent, productName, agentName} (public, both profiles). GET /api/v1/health → {ok, version, agent: "codex"} and /system/status.claude gains agent, agentName — on a Codex instance only: a Claude instance keeps its exact previous payloads ({ok, version}; absent agent ⇒ claude), which the existing test-suite pins; TOTP issuer, WebAuthn RP name and OpenAPI title use the product name. Cookie names are unchanged (different host).
  • Arguments: resume <uuid> / resume --last; resumeConversationId and ids reported by the notify script must be UUIDs.
  • Input ends the wait. Codex's notify has no "prompt submitted" event, so for profiles without a prompt-submit hook the terminal WebSocket proxy reports client → runtime binary frames (never their content) and the first one after WAITING_FOR_INPUT moves the session to RUNNING (terminal_events source input), once per waiting period. Control frames (resize, ping) do not count. This code path is not wired at all for the Claude profile.
  • Static files: /manifest.webmanifest and /icons/<file> are answered from <web export>/brand/codex/… when that file exists (static.ts, brandAsset).

# Web (apps/web)

One build. GET /auth/status.branding decides the product; it is cached in localStorage (cr-branding) and applied before paint by /theme-init.js (<html data-agent="codex">, document title). Codex uses an emerald → teal → cyan accent (html[data-agent='codex'] tokens in globals.css, AA checked by dev/check-contrast.mjs). Capability flags hide what Codex does not have (MCP project scope / SSE / headers / live status, command and sub-agent kinds).

# Limitations / not verified

  • No real Codex turn was run (no paid call). That Codex 0.166 really invokes the legacy notify program at the end of an interactive TUI turn, and the exact payload it sends, are inferred from the binary and the documentation of the mechanism, not observed. The relay, the mapping and the state changes are tested with the real script and a payload of that shape.
  • If ~/.codex/config.toml on the node already has a notify (Codex Desktop's Computer Use sets one), the runtime does not install its own and the instance has no WAITING_FOR_INPUT. Remove the other entry by hand if the signal is wanted, then restart the runtime.
  • "Input ⇒ RUNNING" is a heuristic: any keystroke counts (also one that only edits the prompt), and input typed directly in tmux outside the app is not seen.
  • Approval prompts of Codex do not produce a notify event: such a session stays RUNNING.
  • codex resume --last and codex resume <uuid> were validated against the CLI parser (--help) and launched without a recorded session only; resuming a real session was not exercised.
  • The rollout format, the mcp --json schema and the login status wording are those of 0.166.0; the session filter (thread_source, source) is a heuristic based on four local rollouts.
  • codex mcp add --url may start an OAuth login for servers that support it; the runtime runs it with no terminal and a 30 s timeout.
  • The exported HTML carries the Claude Remote title and description until JavaScript runs.
  • Codex hooks.json (which has a UserPromptSubmit event) is deliberately not used; see above.
Files main
Menu

Theme