# opencode Permissions: allow, ask, deny, and Auto-Approve, Explained

How opencode permissions work: allow/ask/deny rules, last-match-wins, the once/always/reject gate, four ways to auto-approve, and what stays blocked in auto mode.

# 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](#auto-approve-in-opencode-safely) 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](/blog/opencode-permissions-reference); wiring workspace rules in is the [install guide](/integrations/opencode).

[opencode](https://opencode.ai) 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):

```json
{
  "$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](/blog/opencode-permissions-reference#every-key)). 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](/blog/what-survives-yolo-mode).

## 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](/blog/interactive-vs-autonomous-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:

```json
{
  "$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](/controls) — 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](/integrations/opencode) 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](/integrations/opencode) 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](/blog/opencode-permissions-reference#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](/integrations/opencode) 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](/controls/dsh), and [Hermes](/controls/hermes); one place to answer what ran, everywhere, and why.

## Frequently asked questions


