diff --git a/12-Claude-to-OpenCode.md b/12-Claude-to-OpenCode.md index 8e8d544..f9bf5a8 100644 --- a/12-Claude-to-OpenCode.md +++ b/12-Claude-to-OpenCode.md @@ -84,6 +84,58 @@ OpenCode substitutes at load time, in both `headers` and `environment`: 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. +**The trap: `{env:…}` reads OpenCode's process environment, and OpenCode has no env store of its +own.** Claude Code supplies this project's secrets through `env` blocks in its settings cascade — +files opencode never reads. Verified here: a clean login shell +(`env -u VAR zsh -lc …`) has none of `CONTEXT7_TOKEN`, `GITEA_ACCESS_TOKEN`, `GITEA_HOST`; they +exist only inside processes Claude Code spawned. Run `opencode mcp list` from such a process and +everything is green; run it from a real terminal and context7 is `⚠ needs authentication`. + +Worse, the two launch paths disagree on names. A bridge-spawned worker gets `GITEA_TOKEN` + +`GITEA_HOST` from `applyGitToken` (see [Features](11-Features) → the git-forge token grant), not +`GITEA_ACCESS_TOKEN` — so an `opencode.json` written against the terminal's environment fails +inside a worker, and vice versa. + +So the supply route follows from who launches opencode: + +| Launcher | Route | Here | +|---|---|---| +| a human, from a terminal | `{file:.secrets/…}` | gitignored `.secrets/`, workspace-scoped | +| a bridge-spawned worker | `{env:…}` | `applyGitToken` + the profile's `env:` block | + +For the terminal case this repo uses `{file:}` with **workspace-relative** paths — verified: opencode +resolves them against the project root, so no shell export and no direnv is needed, and the secret +never enters an rc file that every process inherits. + +`.secrets/` being gitignored is the point *and* the limitation: a worker in a git worktree does not +receive it, by the same tracked-files-only rule that makes `opencode.json` worth committing. Workers +are fed by the launcher instead — which is also where the name mismatch above must be reconciled. + +### Keeping Claude Code on the same source of truth + +Claude Code expands `${VAR}` in `.mcp.json` but cannot read a file, so left alone it needs the same +tokens duplicated as literals in `.claude/settings.local.json`. A committed `.autoenv` (directory-scoped +env loader — `autoenv` here, `direnv` elsewhere) closes that gap by exporting `.secrets/` into the +environment, holding no secret of its own: + +```mermaid +flowchart LR + S[".secrets/
(gitignored, 0600)"] + S -->|"{file:...}"| OC["opencode.json"] + S -->|".autoenv exports"| E["process env"] + E -->|"${VAR}"| MJ[".mcp.json"] + L["launcher env"] -->|"{env:...}"| WK["spawned worker"] + classDef warn fill:#b7791f,stroke:#7b341e,color:#ffffff; + class L warn +``` + +*One store, three readers — the worker path stays separate because a worktree has no `.secrets/`.* + +Two verified caveats. The loader fires on entering the directory **and** when a shell starts there, +so a terminal opened directly in the repo is covered — but a Claude Code launched outside a login +shell is not, and an unset `${VAR}` degrades silently to the literal text `${VAR}` rather than +failing. And leaving the directory does not unload the variables unless leave-files are enabled. + ## What must never cross Machine-local MCP servers — an IDE index, a language server, an editor bridge. A committed project @@ -101,8 +153,11 @@ A file on disk proves nothing about what the agent loaded. 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. +The answer must reflect the real `CLAUDE.md`. Then `opencode mcp list` — and treat its verdict with +suspicion: **"connected" does not mean "working"**. A stdio server missing its credential completes +the handshake and reports green; only a tool call exposes it. Verified: `gitea` showed +`✓ connected` with no token, then failed the first call with `token is required`. Exercise one +authenticated tool per server, and confirm nothing you withheld appears. ## Gotchas