diff --git a/12-Claude-to-Codex.md b/12-Claude-to-Codex.md deleted file mode 100644 index b1c4271..0000000 --- a/12-Claude-to-Codex.md +++ /dev/null @@ -1,227 +0,0 @@ -# 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**, and Codex discovers it from the working directory -— so its blast radius is decided by one thing: whether you commit it. - -- **Left untracked** (the default the sync leaves you in) it stays local to your own checkout. A - git worktree cut from the project does *not* receive untracked files, so no peer sees it. -- **Committed**, every worktree gets it, and a worker in its own worktree mounts servers whose every - path points back into the primary's checkout. - -The second case 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*. - -So the hazard arms on `git add`, not on `ai-config-sync --apply`, and an untracked file is one -`git add -A` away from armed. Close it explicitly rather than relying on nobody doing that: - -```gitignore -.codex/config.toml -``` - -Do this even if you filtered the sync correctly — the ignore rule protects the *next* person to run -it without `--include`. Note that gitignore is the right tool only while the file is untracked; a -config file already tracked for other reasons needs `git update-index --skip-worktree` instead, -which is how this repo pins `.mcp.json`. - -Port the bridge server. Leave the rest. - -**`status` will report the gap forever, and that is correct.** Once you have filtered, every later -`ai-config-sync status --scope project` shows the withheld servers as unresolved drift: - -``` -project/mcp [safe] missing-in-codex: intellij-index [exact] - missing-in-codex: jetbrains [exact] -``` - -That is the steady state, not a to-do list. The failure mode is a later session reading it as one -and "fixing" it with an unfiltered sync. - -## 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` and keep it out of commits — - gitignore it, per [What must never cross](#what-must-never-cross). "It only has the bridge mount - today" is not a reason to commit it; the file is what the *next* sync writes into. diff --git a/12-Claude-to-OpenCode.md b/12-Claude-to-OpenCode.md new file mode 100644 index 0000000..8e8d544 --- /dev/null +++ b/12-Claude-to-OpenCode.md @@ -0,0 +1,142 @@ +# 12 — Porting a Claude Code workspace to OpenCode + +**What this page is for.** Taking a project a Claude Code session already works in, and making an +OpenCode session a first-class participant in the same workspace — same instructions, same MCP +servers, same bridge mount. + +**The short version: there is almost nothing to port.** OpenCode reads `CLAUDE.md` natively. The +only artifact you create is one `opencode.json` mapping MCP servers. Everything below was verified +against `opencode 1.18.16`; the procedural form ships as the `port-to-opencode` skill. + +## What you get for free + +OpenCode's instruction search order: + +``` +1. walking up from cwd: AGENTS.md , then CLAUDE.md +2. global: ~/.config/opencode/AGENTS.md +3. Claude Code global: ~/.claude/CLAUDE.md (unless disabled) +``` + +> *"The first matching file wins in each category."* + +So a project `CLAUDE.md` **and** your user-level `~/.claude/CLAUDE.md` are already read. No +translation step, no generated second copy, no sync tool — and therefore none of the drift that +comes with them. + +```mermaid +flowchart LR + subgraph W["Claude Code workspace"] + C1["CLAUDE.md"] + C2[".mcp.json"] + C3[".claude/skills/"] + end + subgraph O["OpenCode"] + O1["read natively"] + O2["opencode.json
(mcp)"] + O3["not read"] + end + C1 -->|"no port needed"| O1 + C2 -->|"map — FILTERED"| O2 + C3 -.->|"no equivalent"| O3 + classDef warn fill:#b7791f,stroke:#7b341e,color:#ffffff; + class O3 warn +``` + +*Instructions cross for free; MCP servers need a small mapping; skills do not cross at all.* + +## The one file you write + +Project config is `opencode.json` in the repo root; the global one is +`~/.config/opencode/opencode.json`. **Configs merge rather than replace**, so machine-local servers +belong in the global file and shared ones in the project file. + +| `.mcp.json` | `opencode.json` | +|---|---| +| `"type": "http"` / `"sse"` | `"type": "remote"`, `"url"` | +| `"type": "stdio"` | `"type": "local"`, `"command": ["bin", "arg"]` | +| `"command"` + `"args"` | single `"command"` array | +| `"env"` | `"environment"` | +| `"headers"` | `"headers"` | + +```json +{ + "$schema": "https://opencode.ai/config.json", + "instructions": ["CLAUDE.md"], + "mcp": { + "bridged": { "type": "remote", "url": "http://127.0.0.1:8765/mcp", "enabled": true } + } +} +``` + +Set `instructions` explicitly even though `CLAUDE.md` is found anyway — the fallback applies only +while no `AGENTS.md` exists. + +## Secrets: reference, never embed + +OpenCode substitutes at load time, in both `headers` and `environment`: + +``` +{env:VARIABLE_NAME} from the environment +{file:~/.secrets/token} from a file +``` + +This is what makes `opencode.json` safe to commit — and it **must** be committable, because a peer +running in a git worktree receives tracked files only. No credential ever belongs in the file. + +## What must never cross + +Machine-local MCP servers — an IDE index, a language server, an editor bridge. A committed project +config reaches every worktree, and a worker mounting servers whose paths point into the primary's +checkout will edit the primary's files while building in its own: every build passes, every change +lands in the wrong tree. See [Features](11-Features) → *Worker tool-surface isolation*. + +Keep those in the global config if you want them personally. + +## Verify against the running agent + +A file on disk proves nothing about what the agent loaded. + +```bash +opencode run "In one line: state a rule from this project's instructions." +``` + +The answer must reflect the real `CLAUDE.md`. Then list the MCP servers and confirm each expected +one is connected — and that nothing you withheld appears. + +## Gotchas + +- **A leftover `AGENTS.md` silently shadows `CLAUDE.md`.** If one exists from an earlier port, + delete it, or opencode reads the stale copy instead of the live rules. Check this first. +- **Skills do not port.** OpenCode uses its own agent markdown under `.opencode/agents/`; + `.claude/skills/**` is not read. A delegation brief that says "load the *implementer* skill" has + no effect on an opencode peer — spell the procedure out in the brief instead. +- **Identity comes from the terminal, not the runtime.** A peer must be spawned into its own pane; + running `opencode` in the primary's pane resolves as the primary. +- **The subscription guard doesn't apply.** It is `ANTHROPIC_BASE_URL`-shaped; opencode + authenticates through its own credentials, so `maxLoad` is the only spend control that reaches it. + +--- + +## Appendix — the Codex port (superseded) + +Chapter 12 previously documented porting to OpenAI Codex. That path was abandoned in favour of +opencode, which the bridge already supports through `OpenCodeLauncher`. Four findings are kept +because they were verified by hand and explain *why* the comparison went this way: + +- **Codex has no `CLAUDE.md` fallback.** Instructions had to be translated into `AGENTS.md` by a + third-party tool, whose terminology map rewrote file paths into sentences that were wrong rather + than merely awkward — turning "never commit `.mcp.json`" into a rule about a path in `~/` that + cannot be committed at all. The generated file was never safe to regenerate. +- **Codex config has no variable interpolation.** `${VAR}` passes through literally, so a secret in + a config file had to be a literal value. +- **Codex scrubs the environment for MCP servers.** A stdio MCP child receives only a core set + (`HOME LANG LOGNAME PATH PWD SHELL SHLVL TERM TMPDIR USER …`) — neither the parent environment nor + `shell_environment_policy.set` reaches it. A per-server `env_vars` whitelist exists to pass named + variables through; it was documented and accepted by the CLI, but never confirmed end-to-end here. +- **Project config is gated on trust.** `.codex/config.toml` is read only when the checkout is + marked `trust_level = "trusted"` in the home config, and the two layers merge. + +Contrast with opencode's `{env:VAR}` and native `CLAUDE.md` reading: the same three problems — rules +delivery, secret handling, and tool-surface control — cost one small JSON file instead of an adapter, +a provisioner, and a translation tool. diff --git a/_Sidebar.md b/_Sidebar.md index 045ec32..9ec486f 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -15,7 +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 +12. [Claude → OpenCode](12-Claude-to-OpenCode) — porting a workspace to a second host --- 🟢 herdr-centric `bridged` · AgentAPI = fallback