Switch agents to switch policy
You’ve inherited feedmill — a Go CLI plus a sync server that pulls dozens of differently-shaped RSS, Atom and JSON feeds, dedupes them, and serves a reading queue. Before you change a line you want to understand it: trace how a malformed Atom feed slips past the parser, read the dedupe key logic, walk the cron path. That’s a read-heavy hour, and the last thing you want during it is the agent “helpfully” rewriting a parser you were only trying to read. You want a posture: eyes on, hands off.
Then the audit ends and the work begins — fix the parser, add the test, wire the new source — and now the read-only leash is just friction. You want the opposite posture: a free hand.
In a tool with named profiles you’d be editing a settings file to flip between those two stances. OpenCode puts the policy somewhere you can reach without leaving the prompt: on the primary agent itself. You don’t edit a permission map to go from auditing to building — you switch which agent you’re talking to.
The two agents are two policies
Section titled “The two agents are two policies”OpenCode ships two primary agents, and the only interesting difference between them for this lesson is what they’re allowed to do:
plan— restricted. File edits and bash commands are gated behind approval: they’re set toask, so the agent can read, grep, and reason freely, but any edit or shell command pauses for your explicit go-ahead instead of running on its own.build— the default, with all tools enabled. Edits and bash run end to end, no checkpoint.
Switch between them with the Tab key, right at the prompt — no command, no file, no restart (the keybind is agent_cycle, default tab; shift+tab cycles back). The active agent is surfaced in the TUI so you can see which posture you’re in, which means your permission policy stays visible instead of buried in a config you have to remember the state of.
Start the audit in plan. Tab until the active agent shows plan, then ask the read-only questions:
plan ▸ how does the Atom parser decide an entry is a duplicate of one we've already ingested? walk me through the dedupe key.
⏵ read internal/feed/atom.go ⏵ grep "dedupeKey" internal/feed ⏵ read internal/store/dedupe.go
The dedupe key is built in store.DedupeKey: it hashes the entry's <id> if present, else falls back to the entry link. Atom entries without an <id> and with a tracking-param'd link therefore hash to different keys on each fetch — that's your duplicate leak.That landed you a real finding, and reading never tripped a gate. Now suppose, mid-trace, you reflexively ask it to fix what it just found:
plan ▸ ok, fix it — fall back to a normalized link when <id> is missing.
⏸ edit needs approval (agent: plan) internal/store/dedupe.go [a]pprove [d]eny — or Tab to build to drop the prompt entirelyThat checkpoint is the point. In plan, an edit doesn’t just happen — it stops and waits for you. The policy isn’t advice you’re trusting the model to honor; the agent is configured to pause every write and shell command for a human yes. On code you’ve owned for an hour, that pause is the difference between an audit and an accident. (If you want a wall that can’t be approved away rather than a checkpoint you might absent-mindedly wave through, see the custom agent below.)
Which posture an hour of work deserves is a call you can make before you touch Tab, and it rests on two properties of the work — never on how hard it is. Set them for the audit hour (inherited parsers you only mean to read), then for the fix that follows, and watch the recommended rung move; plan and build are OpenCode’s names for two of the rungs you’ll land on:
Tab carries the thread across the policy line
Section titled “Tab carries the thread across the policy line”Here’s what makes this better than two separate settings files: switching agents doesn’t switch sessions. The conversation comes with you. So the natural move is to do the whole audit in plan, let it propose an approach in words, and only then cross into build to execute — same thread, same context, gated-then-open policy:
plan ▸ outline the fix without writing anything yet.
Plan: 1. In store.DedupeKey, when <id> is absent, normalize the link (strip utm_* and fbclid params, lowercase host) before hashing. 2. Add a table test in dedupe_test.go covering the tracking-param case.
(From plan I'd have to ask before applying this — Tab to build when ready.)
▸ [Tab] agent: build
build ▸ do it.
⏵ edit internal/store/dedupe.go ⏵ edit internal/store/dedupe_test.go ⏵ bash go test ./internal/store/ ok github.com/you/feedmill/internal/store 0.142sYou kept the dangerous part behind an approval gate and relaxed it for the safe part, and you never left the prompt to do it. The plan agent read and proposed; the build agent edited and ran the test — and because it’s one thread, build already knew exactly what the plan was.
When two policies aren’t enough, write a third
Section titled “When two policies aren’t enough, write a third”plan gates everything behind approval and build is wide open, but your audit hour wanted something sharper than both: a hard read-only wall on edits — not a checkpoint you could approve away — and a narrow set of read-only shell commands allowed to run without prompting at all (git log, git blame, go vet). Neither shipped agent is exactly that.
This is where OpenCode’s answer to named profiles shows up. There are no saved “profiles” here in the Codex sense; a custom primary agent is the saved posture. You author one once, and from then on it’s just another agent on the Tab cycle. The fastest way is opencode agent create, which walks you through scope, description, and which permissions to grant, then writes the file for you. Or hand-write it — a primary agent is a markdown file in ~/.config/opencode/agents/ (global) or .opencode/agents/ (this repo only), frontmatter on top, system prompt in the body:
---description: Read-only auditor — reads code and runs read-only git/vet, never editsmode: primarypermission: edit: deny bash: "*": deny "git log*": allow "git blame*": allow "go vet ./...": allow---You are auditing inherited code. Read, trace, and report findings.Never propose edits unless asked; when asked, describe the change inwords and tell the user to switch to build to apply it.The mode: primary line is what puts audit on the Tab cycle alongside build and plan. The permission block is the posture: edit: deny is a hard wall — unlike plan’s ask, there’s no approve button to fat-finger — and the bash map uses OpenCode’s last-match-wins evaluation, so the catch-all "*": deny comes first and the specific allowances after, letting git blame run while rm -rf never does.
Permission values are allow, ask, or deny, and they’re not limited to edit and bash — the documented keys include read, glob, grep, task, skill, lsp, question, webfetch, websearch, external_directory, and doom_loop. (There is no list permission — file listing falls under glob.) Per-agent permissions merge with your global config, with the agent’s rules taking precedence.
Now your audit hour is one keystroke away. Tab to audit to investigate with exactly the latitude you want, Tab to build when it’s time to fix, Tab to plan when you want the approval-gated middle ground. The policy isn’t something you configure each time you need it — it’s something you select, the same way you select which agent does the thinking.
That’s the coarse-grained version: a whole posture per agent, enforced at the agent boundary. But an agent that can’t edit is still an agent that can read every file on your disk and shell out to whatever its bash rules allow. When read-only isn’t a strong enough wall — auditing genuinely untrusted code, or letting a build agent run wide while you sleep — you stop relying on policy and put a real boundary around the whole process. Next: isolate by running in a container.