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_HOMEis honoured for configuration, authentication, sessions and MCP (codex login statusanswers "Not logged in" in an empty one while the real home is logged in).codex --version→codex-cli 0.166.0.codex login statusprints 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 ofcodex/codex resumeis 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-autodoes 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 areresponse_itemrecords withpayload.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.typestdio|streamable_http;envcontains the values in clear;auth_statusunsupported|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.).addon an existing name replaces it silently. One scope (global,config.toml), no SSE, no custom headers from the CLI, no health check inlist. notify: top-level key ofconfig.toml, an argv array; the binary contains the legacy-notify payload fieldstype(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 includeUserPromptSubmit,Stop,SessionStart,PreToolUse, …, configured inhooks.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/skillsand.codex/skills. Instructions:AGENTS.md,AGENTS.override.md. No$CODEX_HOME/promptscustom-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 400forbidden_flag: every bypass flag above (any spelling containingbypassoryolo),-c/--config,--enable,--disable,-p/--profile,--remote. Refused with 400invalid_resume: any other shape — in particular a free positional argument. GET /healthgainsagentandagentHooks: {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 recordedcwdequals 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) andsource: "exec"runs are skipped.GET /skills: kindskillfrom$CODEX_HOME/skills,~/.agents/skills,$CODEX_HOME/skills/.system(sourceplugin= bundled with the CLI), and for a project<root>/.agents/skills,<root>/.codex/skills; kindagentforAGENTS.md/AGENTS.override.md(global and project). Never kindcommand.POST /skills/opworks on skills only (user scope$CODEX_HOME/skills, disabled ones parked in$CODEX_HOME/.disabled/skills; project scope<root>/.agents/)./mcp: list throughcodex mcp list --json(scope alwaysuser, statusunknown, orneeds_authwhenauth_statusisnot_logged_in; "Disabled" aslastError); env values, URL credentials and query strings are dropped.addrefuses SSE, headers and project scope (400unsupported) 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). WithCR_INSTALL_AGENT_HOOKS=1the runtime setsnotify = ["<repo>/apps/runtime/bin/cr-codex-notify"]in$CODEX_HOME/config.tomlby a line-based edit of the top-level key (everything else is preserved byte for byte; file mode kept; atomic write). An existingnotifythat is not ours is never replaced: the file is left untouched,/healthreportsagentHooks.status = "conflict"and/system/statusshows a DEGRADEDagent-hookscomponent. The script is a no-op outside a Codex Remote session; inside it forwards the JSON (stdin of curl, token on fd 3) toPOST /hooks/claude, whereagent-turn-completebecomesStopwiththread-idas 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.claudegainsagent,agentName— on a Codex instance only: a Claude instance keeps its exact previous payloads ({ok, version}; absentagent⇒ 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;resumeConversationIdand 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_INPUTmoves the session toRUNNING(terminal_eventssourceinput), 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.webmanifestand/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
notifyprogram 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.tomlon the node already has anotify(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 --lastandcodex 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 --jsonschema and thelogin statuswording are those of 0.166.0; the session filter (thread_source,source) is a heuristic based on four local rollouts. codex mcp add --urlmay 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 aUserPromptSubmitevent) is deliberately not used; see above.