Skip to content
Agentic Control Plane

opencode permissions and auto-approve, explained

opencode permissions are a permission block in opencode.json: each tool maps to allow, ask, or deny, and bash, edit, read, and a few other keys also take patterns ("git status *": "allow", "rm *": "deny"), resolved last-match-wins. Anything at ask prompts you — once, always, or reject. Auto-approve has four answers of widening scope, from allow patterns to --auto, and every one of them keeps your deny rules standing; that section is the one most people come here for.

This page explains the model — how each mechanism behaves, the interactions that aren’t obvious from the docs, and where the model ends. The key-by-key schema, defaults, config layers, and worked examples are in the opencode permissions reference; wiring workspace rules in is the install guide.

opencode is among the most-starred open-source coding agents (the repo now lives at anomalyco/opencode; the old sst path redirects), and its control model is one of the more thoughtfully shaped: a real rule language, a three-way native gate, an escape hatch that doesn’t waive your denies, and an in-process plugin system that can stand on the permission system itself.

The permission system

Rules live in the permission block of opencode.json (~/.config/opencode/opencode.json globally, or opencode.json at the project root, which loads later and wins for the keys it sets):

{
  "$schema": "https://opencode.ai/config.json",
  "permission": {
    "edit": "ask",
    "webfetch": "allow",
    "bash": {
      "*": "ask",
      "git status *": "allow",
      "rm *": "deny"
    }
  }
}

Three verdicts — allow runs without a prompt, deny blocks, ask fires the native gate — with two granularities: per-tool (read, edit, bash, webfetch, task, skill, and friends) and pattern maps on the keys that take them (bash over the parsed command, edit and read over the file path, webfetch over the URL — the full key table). Resolution is last-match-wins, so ordering is your specificity mechanism: a catch-all "*": "ask" followed by "git status *": "allow" quiets the safe case while keeping the prompt for everything else — and a bare "git status" matches only the bare command, so the star does real work. Per-agent overrides exist too (agent.<name>.permission.*, or frontmatter in agent Markdown), and agent rules take precedence — the built-in plan agent asks on every file edit and every bash command by default.

Know the defaults before trusting them: most tools default to allow. What ships restrictive is narrow and well-chosen — read denies *.env files by default (allowing *.env.example), and two guard pseudo-tools default to ask: external_directory (touching paths outside the project) and doom_loop (the same call repeated three times with identical input — a genuinely clever native tripwire for stuck agents). Everything else is quiet until you write rules.

The pattern language is glob-over-command-string. That’s expressive enough for real policy and carries the standard caveat of every string-match permission system: command strings have many spellings. rm * doesn’t match command rm, an interpreter wrapper, or a deletion spelled through find -delete. Treat patterns as intent-capture for the common spellings, not as a boundary — the same lesson every harness’s denylist has taught.

The native gate: once / always / reject

A tool resolving to ask prompts the human with three answers, and the middle one is the interesting design choice: always approves the pattern for the rest of the session, so the same call shape stops asking. This is approval-fatigue management built into the gate — the third identical yes becomes a session rule instead of a fourth prompt — and scoping it to the session (rather than persisting forever) is the right default.

Worth knowing about always: within its session it’s a silent promotion of an ask into an allow, recorded nowhere reviewable. The prompt UX is genuinely good; what it quietly accumulated over a long session is the part you’ll want an independent record of.

The --auto flag

opencode’s full-auto flag is better-behaved than most of its peers’: --auto skips the ask prompts, but explicit deny rules remain enforced. Compare Cline’s CLI (auto-approve everything, on by default) or the yolo modes that waive whole rule systems: opencode’s escape hatch removes the human, not the policy.

The subtlety: anything you gated only with ask — because the native prompt was your control — silently becomes an allow under --auto. If a rule matters unattended, it has to be a deny (or resolved by a policy layer), because the ask tier evaporates exactly when nobody’s watching. This is the empty-chair test applied to one flag.

Auto-approve in opencode, safely

“How do I stop opencode asking me?” has four answers, and they differ in scope, in what they leave gated, and in whether anything records what you waved through. The binary’s own help text for the widest one is the honest summary: --auto auto-approve permissions that are not explicitly denied (dangerous!) (1.18.4). Narrowest first.

1. Allow patterns — per command shape, permanent, reviewable. The right default. Auto-approve the shapes you’d approve every time and leave the rest at ask; last-match-wins means the broad rule goes first and the exceptions after it:

{
  "$schema": "https://opencode.ai/config.json",
  "permission": {
    "read": "allow",
    "edit": "ask",
    "webfetch": "ask",
    "bash": {
      "*": "ask",
      "git status*": "allow",
      "git diff*": "allow",
      "git log*": "allow",
      "npm test*": "allow",
      "npm run lint*": "allow",
      "rm -rf *": "deny",
      "git push --force*": "deny"
    }
  }
}

This lives in ~/.config/opencode/opencode.json (or per-project at the repo root, which wins for that repo). It’s the only auto-approve that survives a restart and sits in a file you can diff. The standard caveat applies: patterns match the command string, so rm -rf * catches that spelling and not find -delete — put the destructive shapes in deny for intent, not as a boundary.

2. always at the gate — per shape, session-scoped, unrecorded. When the native prompt fires, always approves that call shape until the session ends, then forgets. Good for a long session’s third identical npm test; the thing to know is that nothing outside opencode records what got promoted.

3. --auto — everything at ask, for the run. Every permission that isn’t an explicit deny is approved. Denies still hold, which is why it’s the best-behaved yolo flag on the comparison page — but everything you were relying on the prompt for is now an allow, including edit, external_directory, and doom_loop, unless you wrote a deny. Unattended, this is the flag you’ll reach for; the section below is what still stands under it.

4. Policy pre-approval — the prompt is skipped because a rule said so, and a row says it did. With the ACP plugin installed, every tool whose permission resolves to ask reaches permission.ask before the prompt renders. An ACP allow sets the permission to allow — no prompt, the call runs — and the decision, the tool, the arguments, and the session land in the audit log. An ACP ask leaves the native once/always/reject gate to fire exactly as it would without the plugin. So a workspace rule like “git, npm test, and reads are fine; edits ask; anything touching production asks” is an auto-approve that’s the same on every machine that installed the plugin, instead of one opencode.json per laptop. The permission block still matters: a tool at allow in your config never reaches permission.ask at all, which is why the installer sets bash, edit, and webfetch to ask (filling gaps only, never overriding a value you set).

What stays gated

Whatever you auto-approve, a call still stops when:

  • Your config says deny. Explicit deny patterns hold under always and under --auto.
  • Policy says deny. With the plugin, tool.execute.before runs on every tool call — including tools sitting at allow that never see a prompt — and an ACP deny throws, which is how opencode blocks from that hook. What the agent sees is the reason: [ACP] Denied by policy: <reason>.
  • Policy says ask and someone is there. ACP ask on a tool at ask in your config renders as the native gate. (tool.execute.before has no ask primitive, so an ask verdict reaching a tool at allow can only deny — one more reason to keep the block.)

What always blocks

Under the workspace policy sits the hardline floor: a short list that denies in every workspace, above policy — no tenant setting, tier default, standing approval, always, or --auto can allow it. The bar is “no legitimate agent task ever needs it”: recursive delete of the root filesystem, a system directory, or the home directory (quoting and ${HOME} spellings included); mkfs; dd or a redirect onto a raw block device; a fork bomb; kill -1; shutdown/reboot/halt/poweroff, init 0/6, systemctl poweroff. Commands laundered through sh -c '…' or eval '…' are scanned as commands, not prose. A hit comes back as:

[ACP] Denied by policy: hardline floor: recursive delete of the root filesystem — blocked unconditionally; this pattern cannot be allowed by policy or approval

Everything softer is ordinary policy, which your workspace sets — and which you can widen to auto-approve as far as you like without touching the floor.

Unattended: one more setting

If the gateway is unreachable, the plugin fails open by default — the call proceeds with one loud warning per session — because an ACP outage must never brick an attended session. For an --auto run nobody is watching, flip it: export ACP_FAIL_MODE=closed (or echo closed > ~/.acp/failmode), the same setting every ACP harness integration reads. A policy deny blocks regardless of fail mode; the mode only decides what happens when there’s no answer at all. The installer’s default path provisions a workspace for opencode; it skips opencode under --local, and says so.

The plugin surface

opencode ships a first-class JS/TS plugin system — modules whose exported hooks run in-process. The control-relevant points:

  • permission.ask — fires when a tool’s permission resolves to ask; a plugin can resolve the prompt programmatically (pre-approve, deny, or leave it to the human). This is the rare case of a harness exposing its permission system itself as an extension point, not just the tool calls.
  • tool.execute.before — fires before execution; a throw blocks the call. Deny-only.
  • tool.execute.after — observational; fires after the result exists.
  • Plus a general event bus (permission.asked / permission.replied, command.executed, file.edited, session events) rich enough to observe token usage, costs, and tool results in-process.

Three field-tested caveats. First, the permission hook only fires for tools that resolve to ask — a tool at allow never touches it. A plugin that needs to see every call must pair it with tool.execute.before as a backstop, accepting that the backstop can only deny, not ask. Second: plugin coverage is only as complete as the permission block routing tools into it — config and plugin have to be set up together, which is why our installer writes both. Third: plugins auto-load from the project’s .opencode/plugins/ directory with no registration step — convenient, and it means a cloned repository can ship code that runs inside your agent with your permissions. Treat project plugin directories with the same suspicion as a repo’s install scripts.

What’s not there

  • No sandbox. No OS-level isolation ships with opencode; the boundary is the permission system.
  • No native audit beyond the session log. The run’s own record, on the run’s own machine. The always promotions, in particular, accumulate invisibly.
  • Per-machine config. opencode.json is wherever each install put it; two machines’ effective policies drift silently. The one native answer is the managed config layer (/etc/opencode/, /Library/Application Support/opencode/), which opencode loads last and documents as not user-overridable — it pins the rule on that machine, and records nothing about what the rule did (locking a rule).

Where the native model ends

opencode’s native model is genuinely strong on the attended side: a real rule language, the best-designed escape hatch of the major harnesses, and a permission system a plugin can stand on. The structural limits are the ecosystem’s usual four: the record is self-authored, the policy is per-machine, the ask tier evaporates unattended, and the rules don’t travel to your other harnesses.

That seam is what our plugin fills, using opencode’s own extension points: workspace policy resolves permission.ask (allow pre-approves the prompt away, deny blocks, ask falls through to the native once/always/reject gate — the good UX stays), tool.execute.before backstops the rest, and every decision lands in a ledger off the machine. Same policy on opencode, Claude Code, dsh, and Hermes; one place to answer what ran, everywhere, and why.

Frequently asked questions

How do permissions work in opencode?

A permission block in opencode.json maps tools to allow, ask, or deny — with per-tool entries and bash command patterns like “git status*”: “allow” or “rm *”: “deny”, resolved last-match-wins. A tool resolving to ask fires opencode’s native gate, where the human answers once, always, or reject. Tools not covered by an entry follow the defaults.

How do I auto-approve tool calls in opencode?

Four ways, from narrowest to widest. Allow patterns in the permission block (“git status*”: “allow”) auto-approve one command shape permanently and reviewably. Answering always at the native gate auto-approves that shape for the rest of the session. The --auto flag auto-approves every permission that is not explicitly denied — the binary’s own help text calls it dangerous. And with the ACP plugin, a policy allow sets the permission to allow before the prompt renders, so the call is pre-approved with an audit row and denies still fire. Whichever you pick, a deny always wins.

Does opencode's --auto flag disable permissions?

It skips the ask prompts, but deny rules are still enforced — which makes opencode’s escape hatch better-behaved than most: full-auto mode in many harnesses waives everything, while opencode keeps your explicit denies standing. What --auto removes is the human judgment on the ask tier, so anything you only gated with ask becomes an allow when the flag is on.

What can't be auto-approved in opencode?

Three things hold whatever you do at the ask tier. Explicit deny patterns in your permission block survive --auto. With the ACP plugin, a policy deny throws [ACP] Denied by policy: from tool.execute.before, which runs for every tool call including ones sitting at allow. And ACP's hardline floor — recursive delete of root, home, or a system directory, mkfs, dd onto a block device, fork bombs, shutdown — denies in every workspace and cannot be allowed by policy or approval.

Can a plugin intercept opencode's tool calls?

Yes — opencode ships a first-class JS/TS plugin system whose hooks run in-process, including hooks on the permission system itself (permission.ask) and on tool execution (tool.execute.before/after). One caveat from field experience: permission.ask only fires for tools whose permission resolves to ask, so a plugin that wants to see every call needs tool.execute.before as the backstop, and that hook is deny-only.

What audit trail does opencode keep?

The session log — the run’s own record on the machine that ran it. There’s no native decision ledger recording what was evaluated and why across sessions or machines. opencode’s event bus is rich enough that a plugin can build one (token usage, tool results, and costs are all observable in-process), but nothing ships by default.

Running opencode? See, control, and meter every tool call it makes — a check ahead of every dispatch, local token/cost tracking, and a full audit log, in one command:

macOS · Linux · WSL
curl -sf https://agenticcontrolplane.com/install.sh | bash
Windows · PowerShell
irm https://agenticcontrolplane.com/install.ps1 | iex

Full opencode install guide →  ·  see your first checked call →  ·  free up to 5 agents