Repository Public
1# Codex profile ("Codex Remote")23The same code base runs two products. An instance serves exactly one **agent profile**, selected by4`CR_AGENT` (read by `apps/runtime` and `apps/server`; the web build is shared and learns the profile at5run time):67| | `CR_AGENT=claude` (default, unset) | `CR_AGENT=codex` |8|---|---|---|9| Product / agent name | Claude Remote / Claude Code | Codex Remote / Codex |10| Binary | `CR_AGENT_BIN`, else `CR_CLAUDE_BIN`, else `claude` | `CR_AGENT_BIN`, else `codex` (`CR_CLAUDE_BIN` ignored) |11| Default tmux socket (`CR_TMUX_SOCKET` unset) | `claude-remote` | `codex-remote` |12| Default data dir (`CR_DATA_DIR` unset) | `~/claude-remote-data` | `~/codex-remote-data` |13| Agent session command | `$SHELL -lc 'exec claude [--continue | --resume <id>]'` | `$SHELL -lc 'exec codex [resume --last | resume <uuid>]'` |14| Conversation id rule | `^[A-Za-z0-9][A-Za-z0-9-]{7,63}$` | UUID only |15| WAITING_FOR_INPUT signal | hooks `Notification`, `Stop` (settings.json) | `notify` program → `agent-turn-complete` → `Stop` |16| Back to RUNNING | hook `UserPromptSubmit` | first terminal input from a browser |17| Hook installation | `CR_INSTALL_AGENT_HOOKS=1` or `CR_INSTALL_CLAUDE_HOOKS=1` | `CR_INSTALL_AGENT_HOOKS=1` only |1819The definition is `packages/shared/src/agent.ts` (`AGENT_PROFILES`, capability flags, `Branding`).20With no new variable set, every default is the previous Claude behaviour.2122## Session kind: no new value, no migration2324`SessionKind` stays `'claude' | 'shell'` on the wire, in `RtSessionSpec` and in the database25(`terminal_sessions.kind`, CHECK `kind IN ('claude','shell')`). `'claude'` now means **"the26instance's agent"**: Claude Code on a Claude instance, Codex on a Codex instance. Reasons: the live27database has rows with `kind='claude'`, each instance has its own database so the value is never28ambiguous, and no migration or constraint change is needed. Only labels differ in the UI. The29column `claude_conversation_id` likewise holds the Codex session id on a Codex instance.3031## Verified facts about the Codex CLI (codex-cli 0.166.0, macOS arm64)3233Checked on the development laptop with the real binary and a temporary `CODEX_HOME`; no model call was made.3435- `CODEX_HOME` is honoured for configuration, authentication, sessions and MCP (`codex login status`36 answers "Not logged in" in an empty one while the real home is logged in).37- `codex --version` → `codex-cli 0.166.0`.38- `codex login status` prints on **stderr**: `Logged in using ChatGPT` (exit 0) / `Not logged in`39 (exit 1). The runtime reads stdout + stderr and keeps only a boolean and a label (`chatgpt`,40 `api_key`); the rest of the line (a masked key for API-key logins) is never returned.41- Resume: `codex resume [SESSION_ID]` ("Session id (UUID) or session name"), `codex resume --last`42 ("Continue the most recent session without showing the picker"; filtered by cwd unless `--all`).43 A free positional argument of `codex` / `codex resume` is a **prompt** that runs immediately.44- Approval / sandbox bypass flags present in `codex --help`: `--dangerously-bypass-approvals-and-sandbox`,45 `--dangerously-bypass-hook-trust`, `--approve-for-me`, `-s/--sandbox danger-full-access`,46 `-a/--ask-for-approval never`. Hidden aliases accepted by the parser (tested with `--help`):47 `--yolo`, `--not-so-yolo`. `--full-auto` does not exist in this version (still refused by name).48 Configuration overrides: `-c/--config key=value`, `--enable`, `--disable`, `-p/--profile`.49- Sessions ("rollouts"): `$CODEX_HOME/sessions/YYYY/MM/DD/rollout-<local time>-<uuid>.jsonl`. Line 1:50 `{"type":"session_meta","payload":{"id":<uuid v7>,"cwd":<abs path>,"originator":"codex-tui",51 "thread_source":"user","source":…,"base_instructions":…}}` (≈ 22 kB). User prompts are52 `response_item` records with `payload.type="message"`, `role="user"`,53 `content:[{"type":"input_text","text":…}]`; the first one is an injected `<environment_context>` block.54- MCP: `codex mcp list --json` (array; `transport.type` `stdio` | `streamable_http`; **`env` contains55 the values in clear**; `auth_status` `unsupported` | `not_logged_in` | `bearer_token` | `o_auth`),56 `codex mcp add <name> [--env K=V]… -- <command> [args…]`, `codex mcp add <name> --url <url>57 [--bearer-token-env-var VAR]`, `codex mcp get <name> [--json]` (unknown name: exit 1,58 `Error: No MCP server named '<name>' found.`), `codex mcp remove <name>` (unknown name: **exit 0**,59 `No MCP server named '<name>' found.`). `add` on an existing name **replaces it silently**. One60 scope (global, `config.toml`), no SSE, no custom headers from the CLI, no health check in `list`.61- `notify`: top-level key of `config.toml`, an argv array; the binary contains the legacy-notify62 payload fields `type` (`agent-turn-complete`), `thread-id`, `turn-id`, `cwd`, `client`,63 `input-messages`, `last-assistant-message`. Codex Desktop's "Computer Use" plugin sets this key64 itself (`notify = ["…/SkyComputerUseClient", "turn-ended"]` on the laptop).65- A hooks mechanism also exists (`codex features list`: `hooks stable true`; events in the binary66 include `UserPromptSubmit`, `Stop`, `SessionStart`, `PreToolUse`, …, configured in `hooks.json`),67 but hooks need a persisted **trust** decision made interactively (hence68 `--dangerously-bypass-hook-trust`). It is not used: installing it unattended would mean bypassing69 that trust.70- Skills: `$CODEX_HOME/skills/<name>/SKILL.md`, bundled ones in `$CODEX_HOME/skills/.system/`;71 the binary also references `.agents/skills` and `.codex/skills`. Instructions: `AGENTS.md`,72 `AGENTS.override.md`. No `$CODEX_HOME/prompts` custom-prompt directory is referenced by this version.7374## What the runtime does differently (`apps/runtime`)7576- **Launch** (`codex-args.ts`, `sessions.ts`). Accepted argument lists: none, `resume --last`,77 `resume <uuid>`. Refused with 400 `forbidden_flag`: every bypass flag above (any spelling78 containing `bypass` or `yolo`), `-c/--config`, `--enable`, `--disable`, `-p/--profile`, `--remote`.79 Refused with 400 `invalid_resume`: any other shape — in particular a free positional argument.80- **`GET /health`** gains `agent` and `agentHooks: {requested, status: not_requested | installed |81 conflict | error, detail}`.82- **`GET /claude/info`**: `codex --version` + `codex login status`; `configDir` = `$CODEX_HOME`;83 `agent: "codex"`.84- **`POST /claude/conversations`**: rollouts whose recorded `cwd` equals the requested directory85 (real or lexical path), newest first by mtime, at most 50. Bounded: 400 newest files examined,86 256 KiB read per file for the meta record, 4 MiB per listed session. Sub-agent threads87 (`thread_source` ≠ `user`) and `source: "exec"` runs are skipped.88- **`GET /skills`**: kind `skill` from `$CODEX_HOME/skills`, `~/.agents/skills`,89 `$CODEX_HOME/skills/.system` (source `plugin` = bundled with the CLI), and for a project90 `<root>/.agents/skills`, `<root>/.codex/skills`; kind `agent` for `AGENTS.md` /91 `AGENTS.override.md` (global and project). Never kind `command`. `POST /skills/op` works on skills92 only (user scope `$CODEX_HOME/skills`, disabled ones parked in `$CODEX_HOME/.disabled/skills`;93 project scope `<root>/.agents/`).94- **`/mcp`**: list through `codex mcp list --json` (scope always `user`, status `unknown`, or95 `needs_auth` when `auth_status` is `not_logged_in`; "Disabled" as `lastError`); env values, URL96 credentials and query strings are dropped. `add` refuses SSE, headers and project scope (40097 `unsupported`) and an existing name (409, checked first because the CLI would overwrite).98 `test` = `codex mcp get --json`: it only proves the server is configured.99- **Notify** (`codex-notify.ts`, `bin/cr-codex-notify`). With `CR_INSTALL_AGENT_HOOKS=1` the runtime100 sets `notify = ["<repo>/apps/runtime/bin/cr-codex-notify"]` in `$CODEX_HOME/config.toml` by a101 line-based edit of the top-level key (everything else is preserved byte for byte; file mode kept;102 atomic write). An existing `notify` that is not ours is **never replaced**: the file is left103 untouched, `/health` reports `agentHooks.status = "conflict"` and `/system/status` shows a104 DEGRADED `agent-hooks` component. The script is a no-op outside a Codex Remote session; inside it105 forwards the JSON (stdin of curl, token on fd 3) to `POST /hooks/claude`, where106 `agent-turn-complete` becomes `Stop` with `thread-id` as conversation id. The prompt and the107 assistant message contained in the payload are not kept.108109## What the server does differently (`apps/server`)110111- `GET /auth/status` → `branding: {agent, productName, agentName}` (public, both profiles).112 `GET /api/v1/health` → `{ok, version, agent: "codex"}` and `/system/status.claude` gains `agent`,113 `agentName` — **on a Codex instance only**: a Claude instance keeps its exact previous payloads114 (`{ok, version}`; absent `agent` ⇒ claude), which the existing test-suite pins; TOTP issuer, WebAuthn RP115 name and OpenAPI title use the product name. Cookie names are unchanged (different host).116- Arguments: `resume <uuid>` / `resume --last`; `resumeConversationId` and ids reported by the117 notify script must be UUIDs.118- **Input ends the wait.** Codex's notify has no "prompt submitted" event, so for profiles without119 a prompt-submit hook the terminal WebSocket proxy reports client → runtime **binary** frames120 (never their content) and the first one after `WAITING_FOR_INPUT` moves the session to `RUNNING`121 (`terminal_events` source `input`), once per waiting period. Control frames (resize, ping) do not122 count. This code path is not wired at all for the Claude profile.123- Static files: `/manifest.webmanifest` and `/icons/<file>` are answered from124 `<web export>/brand/codex/…` when that file exists (`static.ts`, `brandAsset`).125126## Web (`apps/web`)127128One build. `GET /auth/status.branding` decides the product; it is cached in `localStorage`129(`cr-branding`) and applied before paint by `/theme-init.js` (`<html data-agent="codex">`, document130title). Codex uses an emerald → teal → cyan accent (`html[data-agent='codex']` tokens in131`globals.css`, AA checked by `dev/check-contrast.mjs`). Capability flags hide what Codex does not132have (MCP project scope / SSE / headers / live status, command and sub-agent kinds).133134## Limitations / not verified135136- **No real Codex turn was run** (no paid call). That Codex 0.166 really invokes the legacy `notify`137 program at the end of an interactive TUI turn, and the exact payload it sends, are inferred from138 the binary and the documentation of the mechanism, not observed. The relay, the mapping and the139 state changes are tested with the real script and a payload of that shape.140- If `~/.codex/config.toml` on the node already has a `notify` (Codex Desktop's Computer Use sets141 one), the runtime does not install its own and the instance has **no WAITING_FOR_INPUT**. Remove142 the other entry by hand if the signal is wanted, then restart the runtime.143- "Input ⇒ RUNNING" is a heuristic: any keystroke counts (also one that only edits the prompt), and144 input typed directly in tmux outside the app is not seen.145- Approval prompts of Codex do not produce a notify event: such a session stays `RUNNING`.146- `codex resume --last` and `codex resume <uuid>` were validated against the CLI parser147 (`--help`) and launched without a recorded session only; resuming a real session was not exercised.148- The rollout format, the `mcp --json` schema and the `login status` wording are those of 0.166.0;149 the session filter (`thread_source`, `source`) is a heuristic based on four local rollouts.150- `codex mcp add --url` may start an OAuth login for servers that support it; the runtime runs it151 with no terminal and a 30 s timeout.152- The exported HTML carries the Claude Remote title and description until JavaScript runs.153- Codex `hooks.json` (which has a `UserPromptSubmit` event) is deliberately not used; see above.154