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.
┗━ approve? y / N / s / a / e (Enter = no) »| key | does |
|---|---|
y | approve the batch |
N / Enter | reject — the fail-closed default; anything unrecognized also rejects |
s | select per call |
a | always-allow: relax these tools for the rest of the turn (see grants) |
e | explain: 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 changeinstead of a full-file diff; an existing binary file is named as binary; a path the workspace sandbox will refuse is flaggedREFUSEDat 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.
/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.