::docs :: concepts

the approval gate

Risk tiers, the y/N/s/a/e prompt, what a grant covers and how long it lives, shell prefix allowlists, and /policy — the one front door for every relaxation.

Every tool declares a risk tier: read_only, side_effecting, or destructive. Tools at or below the auto-approve threshold (runtime.auto_approve, default read_only) run without prompting. Everything else stops the turn and asks you — with the real artifact of the decision on screen.

the prompt
  ┗━ approve? y / N / s / a / e  (Enter = no) »
keydoes
yapprove the batch
N / Enterreject — the fail-closed default; anything unrecognized also rejects
sselect per call
aalways-allow: relax these tools for the rest of the turn (see grants)
eexplain: the plan step this call fulfils and the model's recorded reasoning, then re-prompt

::what the prompt shows

  • File writes render as a colored unified diff against the current file. A byte-identical rewrite reads no change instead of a full-file diff; an existing binary file is named as binary; a path the workspace sandbox will refuse is flagged REFUSED at the prompt.
  • Shell commands render in full, untruncated, byte-faithfully — tabs and space runs reach you exactly as the shell would receive them.
  • Everything else (notably every mcp_* tool) renders its arguments full-width: for a tool with no bespoke safety surface, the arguments are the safety surface.
  • A secret scan warns inline when a call's arguments carry a key, token, or private-key block — approving the call sends the secret wherever the call goes.
  • If a preview fails to draw, a plain view names the call and the same reject-by-default prompt runs. The prompt always renders.

Rejection is plan-aware: the decline is recorded onto the current step, the remaining steps are cancelled, a declined call is never re-issued, and the answer discloses what was not done. Every prompt is recorded as a structured gate event — it feeds /trace answer, headless --json, and the trust receipt. An empty record always means the human was never asked.

::grants have a lifetime

Answering a grants for the rest of the current turn by default (runtime.grant_scope: task): the tool's tier drop expires at the turn boundary and the turn's closing note says what expired. session keeps a grant until Saturn exits; persist writes it to permissions.json. The scope is a trust setting — session-only unless you pass --save.

For run_shell, a never drops the tool's tier (that would un-gate every future command from one keypress). Instead it offers a prefix grant covering the full command you just reviewed — or a shorter prefix you type deliberately.

::shell prefix allowlist

/policy allow <prefix> persists a run_shell prefix that runs without prompting. Matching is strict: token-boundary, case-insensitive, and never when the command contains shell metacharacters — chaining and redirection always face the human. The arguments after the granted prefix are screened at every use:

  • capability-introducing flags (--output, -c, --exec, …), globs, and paths outside the workspace disqualify the command;
  • a general-purpose interpreter (python, npm, powershell, …) is only ever exempt as the exact granted command;
  • non-ASCII text (a lookalike ) never passes the automation path.

Previously git log --output=<path> could ride in on a git log grant. Now it prompts.

::/policy — one object

Every relaxation mechanism — the threshold, Shift+Tab cycling, per-tool overrides, the prefix allowlist, headless --yolo — is a view of one policy object, and /policy is its front door. Bare forms report; changing is always an explicit verb.

usage
/policy                             the live posture
/policy risk <tool> <tier> [--save]  override a tool's tier (reset restores it)
/policy allow <prefix>              persist a run_shell prefix grant
/policy allow list | remove <n>     inspect / revoke
/policy open [on|off]               open the gate (threshold → destructive)

Durable state is one JSON file, database/permissions.json. A hand-edited file with wrong-shaped fields fails closed like a garbled one: strict defaults, recorded at startup, the file kept aside as .corrupt. Relaxing a tool's tier never removes it from the injection quarantine's coercion scan.

note :: careful

The ⚠ GATE OFF status-bar indicator is derived live from the threshold — there is no separate flag to drift. Choosing a tier explicitly while the gate is open supersedes the pre-open snapshot, so /policy open off lands on the tier you set last.