OpenCode: a read-only Plan pass that cannot touch your files
Separate think from touch: lock the plan agent to read-only (edit + bash deny) so it proposes before it edits, then Tab into build to execute.
Run this workflow
CI-verified, 2/2 fixtures passing.
Build this with your agent
One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.
Intended Use
Anyone exploring an unfamiliar repo who wants zero chance of a write. CI validates that opencode.json parses and the plan agent's permission has edit=deny and bash=deny (build present). No key, no model call. The actual analysis is a fenced model step. Fill the model from `opencode models`.
Not for
- Letting plan run read-only shell like git diff, a blanket bash=deny blocks that; use the per-command form to allow git diff/log
- Expecting CI to grade the proposal, only that the config locks plan to read-only
The Stack
Tested Against
opencode@1.17.4opencode.ai/docs (2026-06)node@20.xSide effects & data flow
- Network
- none, local only
- Writes
- ./opencode.json
- Credentials
- none required
Prerequisites
- OpenCode installed
- A provider logged in (opencode auth login) to actually run it
Steps
- 1
Harden the plan agent to read-only and validate
Pin models for build and plan, and set plan's permission edit=deny and bash=deny so an exploration pass cannot write. Tab into plan to get a proposal, then Tab into build to execute. CI parses opencode.json and asserts plan is read-only.
cat > opencode.json <<'JSON' { "$schema": "https://opencode.ai/config.json", "agent": { "build": { "mode": "primary", "model": "provider/your-strong-model", "permission": { "edit": "allow", "bash": "allow" } }, "plan": { "mode": "primary", "model": "provider/your-cheaper-model", "permission": { "edit": "deny", "bash": "deny" } } } } JSON node -e 'const c=JSON.parse(require("fs").readFileSync("opencode.json","utf8"));const p=c.agent.plan.permission;if(p.edit==="deny"&&p.bash==="deny"&&c.agent.build){console.log("config OK: plan is read-only (edit=deny, bash=deny), build present")}else{console.log("BAD");process.exit(1)}' - 2
Run the plan → build flow (the model step, not checked by CI)
Tab into plan to analyze and get a proposal, then Tab into build to execute it. If you want plan to inspect history, swap the blanket deny for a per-command bash map ({"*": "deny", "git diff": "allow", "git log*": "allow"}). The analysis runs the model, so CI never claims it.
Eval, 2 fixtures
Last passed: verified todayplan-readonlycontainstimeout 30s · max $0Expected:
config OK: plan is read-only (edit=deny, bash=deny), build presentclean-exitexit_codetimeout 30s · max $0Expected:
0
Results
One config block, the habit that prevents the most damage: an agent proposing on a repo it cannot accidentally rewrite. OpenCode is ~174k stars, MIT.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Related workflows
- One AGENTS.md, no drift: prove CLAUDE.md is a real symlink and the generated files are in sync
- Self-host CodeWiki on private code: validate the config before you spend tokens
- Run GLM-5.2 for the bulk, escalate the hard turns to Opus 4.8
- Teach OpenCode Go your weekly chore once, then run it in minutes
- codebase-memory-mcp: wire the knowledge graph, stop re-reading files
- OrcaRouter for coding: judge by passing tests, not by vibes
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).