wiki: chapter 12 — porting a Claude Code workspace to Codex

Written as a procedure so it converts into a plugin skill without rewriting.
Every claim is from a hand-verified run against codex-cli 0.147.0 and
ai-config-sync-manager 0.1.10, not from documentation.

The two steps most likely to be skipped are the ones that cost us: filtering the
MCP sync so machine-local IDE servers never reach a project-root file that every
worktree inherits, and diffing the generated AGENTS.md every time — the
terminology map rewrites file paths into sentences that are wrong rather than
awkward, including turning "never commit .mcp.json" into a rule about a path in
~/ that cannot be committed at all.

Also records the three Codex seams a launcher cannot guess: --approve-for-me is
mandatory for unattended MCP calls, a fresh CODEX_HOME has no credentials and
fails as an opaque mid-run 401, and AGENTS.md is the only way to deliver a
standing instruction since Codex has no --append-system-prompt.
Dai Ha
2026-08-12 15:52:39 +02:00
parent 45cb3ec8c4
commit 26443d64e3
2 changed files with 198 additions and 0 deletions
+197
@@ -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<br/>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 <the project>
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:<your-bridge-server-name> --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:<your-bridge-server-name> --apply
```
Backups land under `~/.ai-config-sync-manager/backups/<timestamp>/`.
### 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 <json>` | `-c mcp_servers.<n>.url="…"`, or `codex mcp add <n> --url` |
| MCP bearer token | server config | `--bearer-token-env-var <ENV>` |
| Unattended tool calls | permission settings | **`--approve-for-me`** |
| Resume a conversation | `--resume <uuid>` | `codex resume <uuid \| name>` — **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.
+1
@@ -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