0146 — One Auto review per action
Status: accepted · 2026-10-26
- Status: accepted
- Date: 2026-10-26
- Builds on: ADR 0100 (Auto's risk policy, the second look), ADR 0117 (judged beside what the person asked), ADR 0118 (a step in someone else's app), ADR 0128 (the person's words, a hold), ADR 0144 (task-aware review)
- Amends: ADR 0144's "a held owner instruction rejects the action" and "neither model verdicts
nor clean-tree checks are cached across calls"; ADR 0100's and ADR 0118's separate second
looks (
risk-look.ts, removed) - Works with: ADR 0147 (the fast reviewer each provider declares), ADR 0149 (the environment note)
Context
One action in Auto could pass up to three model reviews in a row, with two rubrics, and the same chain again on the permission path:
- the second look's
serves(judgeCommand), which could lift a question asked only for what was read; - the same look again as
secondLook, for a command the rules found nothing in, run afterserveslifted; judgeStep(lookAtAppStep), for a step in someone else's app;autoDecision(reviewAutoAction), the task-aware review of ADR 0144, on whatever question was left.
The looks had 6 s deadlines and the task review 8 s, so a command after reading could wait
20 s on the guard's path alone. Claude Code asks through its PreToolUse hook and again through
canUseTool, and nothing was reused between them: up to six calls and 40 s for one step,
each on the turn's own model, which is often the largest one. Two rubrics could disagree
about the same step. A step the person had said not to do ("don't push yet") was refused with
"do not retry", where AGENTS.md security rule 11 says it asks until they say otherwise.
Decision
One reviewer. auto-review.ts has one system prompt and one strict schema,
{decision, risk, reason, kind, evidence}, for a command and a step in an app alike. The
rubric is ADR 0144's (owner messages with ids, evidence, allow, replan or ask) with ADR 0117's
judgement after reading folded in: what the chat read may supply details, never permission;
an action is unasked when it serves nothing in the request and looks steered by what was read.
kind names the risk, so the card says why in Conch's words (verdictWords, the second
look's old phrases). The Gmail send-boundary review is the same reviewer, given the
connector-verified sender.
The same hard guarantees. Strict JSON with duplicate keys rejected; no allow that is severe
or unknown, cites no owner message, or names a kind of risk. Without owner messages there is
no allow, so there a review can only add a question. Bands are where they were: the task review
lifts only what reviewableRisk allows, serves only a command question asked for what was
read (contextualRisk), and where the rules found nothing the review only adds a question
(its allow there just means none). A mutation of the arguments, the owner's words or the mode
during review, and Stop, invalidate it. Off and explicit Ask are read again after it. The
browser's page is checked again before a click. AutoReplans still ends a run of unsafe
proposals; once it has, serves lifts nothing more either.
Two stages, as Claude Code's auto mode classifies. When the turn's provider declares a fast
reviewer (deps.fastReview, ADR 0147), it is asked first, with the same rubric and one short
instruction: allow only what is clearly routine and authorized. Its allow stands, under every
guarantee above. Anything else, or no answer, goes to the turn's own model (deps.autoReview)
with the full rubric. The fast stage never refuses or asks by itself. It gets half the full
stage's 8 s deadline, so the worst case for an action is 12 s, once.
Start early, reuse narrowly. The guard (PreToolUse) starts the review of a call that is
likely to need one, before the undo snapshot and the rest of its checks, and every later look
at the same action awaits the same promise, through to the permission question. Verdicts are
kept for the turn, keyed by reviewKey: the tool and its canonical arguments, the mode, a hash
of the owner's words, what the chat read, the environment note, what the rules found, the app
step, and the sha256 of every file the command could run (python3 x.py, node x.mjs,
./x, pnpm exec tsx x.ts, a project's package.json or Makefile). A file that isn't there
is a state too; one that can't be read binds the verdict to that one call. Any change is a new
review. A mode change forgets every verdict, and Stop ends the turn and them. OpenClaw binds an
approval to the hashes of what it runs; Codex's guardian reuses a verdict while the
authorization evidence is unchanged. Conch does both, within one turn.
A hold asks. A step the person said not to do asks them, on a card that says so: "You asked me not to push code to a remote. So I'm checking before I do it now." Their Allow is for this once. No review lifts a hold, and none refuses it.
The environment note. preferences.autoEnvironment, which only a person writes in
Settings (ADR 0149), goes to the reviewer as owner-written context, quoted as data apart from
the owner's messages. It may say which hosts, remotes and buckets are theirs and what
production means. It can't be cited as evidence and never authorizes a hard rule.
Each stage reports how it ended and how long it took (deps.reviewed, numbers only). The
judged counters keep their meaning.
Threat model
The reviewer reads a page's words and the agent's arguments, so it can be steered. A steered
allow is bounded as before: keys, a stranger's code, money, an assistant's own permission
files, the circuit breaker, NEVER risks, protected paths, tools turned Off, explicit Ask,
guests and holds never reach a lift. The fast stage is a smaller model and easier to steer; it
gets the same bands, may only allow, and isn't asked without owner messages. Tests make both
stages answer allow to the adversarial corpus: what the hard rules mark still asks.
A cached verdict could be replayed for a different action. The key covers everything the verdict judged; the arguments are copied before review, so a change while it runs can't reach the verdict, and the key is computed from the copy. Reuse stays inside one turn.
A script can change between review and execution. Hash binding means a changed script is a new review; it does not make execution atomic, which ADR 0144 already says. The scan of what a command runs is a guess on the wide side: a module a script imports isn't hashed. That is accepted because the reviewer never read the script's contents anyway (ADR 0144).
Starting early spends a model call on a guess: a call the later paths don't need is wasted money, never a weaker decision.
This is stricter in three places, each because a hard guarantee says so. A mail with a key in it at Gmail's send boundary is no longer lifted by the step look's "not risky". A question asked only for what was read needs an owner message as evidence to be lifted, so a task without owner words (until ADR 0149 gives tasks theirs) keeps its question. After three unsafe proposals in a turn, a command question is no longer lifted.
Consequences
One model call per action, usually from the guard, often on a fast model. The guard and the
permission question agree, because they read the same verdict. risk-look.ts is gone; the
riskLook dependency is only the older name for autoReview. Engines without a fast
reviewer run the full stage alone, as before.
Sources
- Anthropic, Claude Code auto mode: a fast first-stage classifier, and a second stage with reasoning only for what the first flags; the second clears work the person asked for.
- OpenAI Codex, the guardian approvals reviewer (V2): reuses a low-risk verdict for its session while the authorization evidence it was given is unchanged.
- OpenClaw c9db73543a42d3e53e23c8b70c3997bc5d4a0fff,
src/infra/exec-approvals-authorization.kernel.ts: an approval bound to the hashes of the executable and files it runs. - ADR 0144: task authority, evidence and the replanning budget.
- OWASP LLM Prompt Injection Prevention Cheat Sheet; Greshake et al., 2023 (indirect prompt injection); Hines et al., 2024 (spotlighting, datamarking).