CodingOpen SourceFreeActiveMachine-verified· beginner · ~5 min setup

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.

by Shilpa Mitra· verified today· v1.0.0

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.x

Side 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. 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. 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 today
  • plan-readonlycontainstimeout 30s · max $0

    Expected: config OK: plan is read-only (edit=deny, bash=deny), build present

  • clean-exitexit_codetimeout 30s · max $0

    Expected: 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.

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).