diff --git a/12-Claude-to-Codex.md b/12-Claude-to-Codex.md new file mode 100644 index 0000000..95f996a --- /dev/null +++ b/12-Claude-to-Codex.md @@ -0,0 +1,197 @@ +# 12 — Porting a Claude Code workspace to Codex + +**What this page is for.** Taking a project that a Claude Code session already works in, and making +an OpenAI Codex session a first-class participant in the same workspace — same instructions, same +skills, same bridge mount. It is written as a procedure so it can become a plugin skill verbatim. + +**Why it exists.** The bridge is a provider-neutral message bus, but a peer is only useful if it +knows the same rules. A Codex session with no ported config is not a peer — it is a stranger with +shell access. Everything below was verified by hand against `codex-cli 0.147.0` and +`ai-config-sync-manager 0.1.10`; nothing here is inferred from documentation. + +## The mapping + +Both hosts express the same four concepts in different files. + +| Concept | Claude Code | Codex | +|---|---|---| +| Instructions | `CLAUDE.md` | `AGENTS.md` | +| Skills | `.claude/skills/` | `.agents/skills/` (project) · `$CODEX_HOME/skills/` (user) | +| MCP servers | `.mcp.json` | `[mcp_servers.*]` in `config.toml` | +| Settings | `.claude/settings.json` | `config.toml` | +| Config isolation | `CLAUDE_CONFIG_DIR` | `CODEX_HOME` | + +```mermaid +flowchart LR + subgraph C["Claude Code workspace"] + C1["CLAUDE.md"] + C2[".claude/skills/"] + C3[".mcp.json"] + end + subgraph X["Codex workspace"] + X1["AGENTS.md"] + X2[".agents/skills/"] + X3[".codex/config.toml"] + end + C1 -->|"port"| X1 + C2 -->|"port"| X2 + C3 -->|"port — FILTERED"| X3 + C3 -.->|"machine-local servers
must NOT cross"| STOP["blocked on purpose"] + classDef danger fill:#b7791f,stroke:#7b341e,color:#ffffff; + class STOP danger +``` + +*Three of the four categories port wholesale. The MCP category is the one that must be filtered — +see [What must never cross](#what-must-never-cross).* + +## Procedure + +### 1. Install the porting tool + +```bash +npm install -g ai-config-sync-manager +ai-config-sync --version +``` + +It performs a continuous bidirectional sync with backups and a diff-first workflow. For this +procedure we use it **one-directional and project-scoped only**. + +### 2. Inspect before changing anything + +```bash +cd +ai-config-sync status --scope project --compact +``` + +Read every diff it reports. `--scope project` is deliberate: the global scope syncs your *user* +configuration between the two CLIs, which is a different decision with different blast radius (see +[Scope](#scope-project-vs-global)). + +### 3. Dry-run with an explicit include list + +Never sync "everything". Name what crosses: + +```bash +ai-config-sync sync --scope project --from claude --to codex \ + --include instructions,skills,mcp: --dry-run +``` + +The include syntax is `category` or `category:item` — so `mcp:bridged` ports exactly one MCP +server and leaves the rest behind. Read the change preview and the patch preview before applying. + +### 4. Apply + +```bash +ai-config-sync sync --scope project --from claude --to codex \ + --include instructions,skills,mcp: --apply +``` + +Backups land under `~/.ai-config-sync-manager/backups//`. + +### 5. Review the generated instructions — every time + +**The terminology map is naive about file paths, and it will produce sentences that are wrong +rather than merely awkward.** Diff the result and read the changed lines: + +```bash +diff CLAUDE.md AGENTS.md +``` + +Two real examples from this repo's port: + +| Generated | Why it is wrong | +|---|---| +| `Never commit ~/.codex/config.toml [mcp_servers]` | The source rule was *"never commit `.mcp.json`"* — a repo file. You cannot "never commit" a path in `~/`, so the sentence lost both its meaning and its protection. | +| skills row rewritten to `.codex/skills/**` | The tool had just written the skills to `.agents/skills/`. The document contradicted the tool's own output. | + +Fix these by hand. **`AGENTS.md` is a generated artifact that is not safe to blind-regenerate** — +re-running the sync reintroduces the same errors, so the diff review is part of the procedure, not +an optional polish step. + +### 6. Verify against the running agent, not the file + +A file on disk proves nothing about what the agent read. Ask it cold, in a fresh session: + +```bash +codex exec -s read-only "In one line: what is the ONLY channel you may use to answer the sender?" +``` + +The answer must reflect your ported instructions. If it answers generically, the instructions did +not reach the model and every later step is built on sand. + +Confirm the MCP mount separately: + +```bash +codex mcp list # your bridge server, status `enabled` +``` + +## What must never cross + +**Machine-local MCP servers.** An IDE index, a language server, an editor bridge — anything bound +to *your* checkout — must stay out of the ported config. + +The generated file lands at the **project root**, which means every git worktree cut from that +project inherits it. A worker in its own worktree would then mount servers whose every path points +back into the primary's checkout. This is not hypothetical: it produced a worker that made all 59 +of its edits in the primary's tree while running its build in its own — so every build passed, and +every change went nowhere. See [Features](11-Features) → *Worker tool-surface isolation*. + +Port the bridge server. Leave the rest. + +## Scope: project vs global + +| Scope | What it touches | Use it for | +|---|---|---| +| `project` | `CLAUDE.md`/`AGENTS.md`, `.claude/skills`/`.agents/skills`, `.mcp.json`/`.codex/config.toml` | making one workspace bi-host — **the procedure on this page** | +| `global` | `~/.claude/**` ↔ `~/.codex/**` | keeping the two CLIs *you* drive interactively in step | + +Keep global sync away from anything the bridge spawns. The bridge deliberately gives each peer an +isolated tool surface; a user-level bidirectional sync works against that by design, not by bug. + +## What Codex does differently + +Porting config is not the whole story — Codex reaches the same outcomes through different seams. +A launcher or skill that assumes Claude's flags will fail silently. + +| Need | Claude Code | Codex | +|---|---|---| +| Standing instruction | `--append-system-prompt` (inline) | **`AGENTS.md` in `$CODEX_HOME`** — a file, no flag exists | +| Mount an MCP server | `--mcp-config ` | `-c mcp_servers..url="…"`, or `codex mcp add --url` | +| MCP bearer token | server config | `--bearer-token-env-var ` | +| Unattended tool calls | permission settings | **`--approve-for-me`** | +| Resume a conversation | `--resume ` | `codex resume ` — **names work natively** | +| Isolate config | `CLAUDE_CONFIG_DIR` | `CODEX_HOME` | +| Pick a model | `--model` | `-m` | + +Three of these are load-bearing enough to call out: + +**`--approve-for-me` is mandatory for any unattended peer.** Without it, every MCP tool call +returns `user cancelled MCP tool call` — even with `approval: never` and a read-only sandbox. MCP +tools sit behind a separate trust gate. The flag routes approvals through automatic review and +**keeps the sandbox on**; it is not the same as +`--dangerously-bypass-approvals-and-sandbox`, which disables sandboxing entirely and should not be +used to work around this. + +**`CODEX_HOME` is a complete boundary, and a fresh one has no credentials.** It holds config, +sessions, skills, plugins, and state. Point a peer at a new empty home and Codex fails with an +opaque `401 Unauthorized` mid-run rather than a startup error — so a launcher must provision +credentials into the home, and should **copy rather than symlink** so a peer cannot write back +through to the operator's real credential. + +**`AGENTS.md` in `$CODEX_HOME` is genuinely read and obeyed.** Verified by planting a distinctive +codeword as a standing instruction and asking an unrelated question in a fresh session; the answer +came back with the codeword. This is the only delivery mechanism for a reply charter on Codex. + +## Gotchas + +- **Identity comes from the terminal, not the runtime.** A Codex process started inside the + primary's pane resolves as the **primary** and inherits its authority. A Codex *peer* must be + spawned into its own pane — you cannot create one by running `codex` in a terminal. +- **The subscription guard does not apply.** The guard is `ANTHROPIC_BASE_URL`-shaped; Codex has no + such seam and authenticates through its own credentials. Codex peers bill their own provider, and + `maxLoad` is the only spend control that reaches them. +- **Model selection lives in the host's own config, not the bridge.** Porting instructions does not + port model choice, and the bridge should not own it — see [Features](11-Features) for why a second + source of truth hides drift instead of fixing it. +- **`.codex/config.toml` is local config.** Treat it like `.mcp.json`: keep it out of commits unless + it contains only the bridge mount. diff --git a/_Sidebar.md b/_Sidebar.md index 342196d..045ec32 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -15,6 +15,7 @@ 9. [Implementation](9-Implementation) — as-built code map · classes · flows · state machines 10. [Cross-Host Messaging](10-Cross-Host-Messaging) — broker topology · exchanges · queues per entity 11. [Features](11-Features) — what it can do · the knob that turns it on · why · the gotcha +12. [Claude → Codex](12-Claude-to-Codex) — porting a workspace to a second host --- 🟢 herdr-centric `bridged` · AgentAPI = fallback