Guardian: Let One Model Approve Another
When approval popups pile up until people tap Yes on autopilot, Codex hands the decision to a locked review session. Timeouts, bad JSON, and consecutive denials each have their own ending.
on-request approvals the config named, and replaces the user popup with a locked model session. Review material is treated as untrusted evidence; a timeout, a crash, or bad JSON stops the action. Only an explicit denial enters the breaker; consecutive denials to the threshold abort the current turn.
- Decide whether this approval goes to Guardianreview.rs L186
- If an extension claims takeover, do not open a review sessionreview.rs L313
- Filter the transcript; keep user intent and tool evidenceprompt.rs L506
- Assemble review material as Full or Deltaprompt.rs L89
- Clone the parent config, lock it read-only, write Neverreview_session.rs L1407
- If trunk is idle, append; if busy, forkreview_session.rs L617
- Wait for JSON; timeout cap is 90 secondsmod.rs L53
- No legal JSON is a parse fail; retry, then close the gateprompt.rs L714
- allow lets it through; deny writes a no-reroute instructionreview.rs L717
- Only an explicit Deny enters the breakerreview.rs L711
- Hit the consecutive or window threshold, then InterruptTurnmod.rs L176
policy.md, to show the structural gap between the four-step method and the breaker. Line numbers on the logic trail match openai/codex commit 4f39251a01.You have Codex open on a repo. The model runs git status, reads two files, then wants git push. A minute later it wants curl, then a note in /tmp, then that note deleted. Popups stack. People start reading only the first two tokens; the thumb already has muscle memory.
That’s approval fatigue. The fence is still there; the judgment is gone. The loop decays into a button that always goes green. Hand auto-allow to the same main model that just wrote the command, and the applicant is also the approver. Allow on failure, and a timeout or bad JSON becomes a free pass.
Codex pulls whether to auto-allow into a dedicated review. It does not replace the sandbox, or the network proxy. It only takes over one class of on-request approvals that would have popped to the user. The route is narrow: the policy must be OnRequest or Granular, and the reviewer must be AutoReview. The default reviewer is still the user.Source:codex-rs/core/src/guardian/review.rs lines 186–210
The module header writes the four steps in stone.
Keep the user’s own words and recent tool traces. Drop synthetic scaffolding and ordinary developer messages. Over-long content gets a truncated mark.
Clone the parent config; inherit the managed network and allowlist already built. The reviewer must emit strict JSON.
A timeout, a crash, or bad JSON stops execution. An in-between state is treated as a deny.
The contract wants allow or deny. The product wants an executable call.
The contract has only four fields. The enums on the three axes live in protocol-layer approvals.rs, not protocol.rs. outcome is the only required field. A low-risk allow can return just {"outcome":"allow"}; missing risk fills in as Low, authorization as Unknown.
/// Structured output contract that the guardian reviewer must satisfy.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub(crate) struct GuardianAssessment {
pub(crate) risk_level: codex_protocol::protocol::GuardianRiskLevel,
pub(crate) user_authorization: codex_protocol::protocol::GuardianUserAuthorization,
pub(crate) outcome: GuardianAssessmentOutcome,
pub(crate) rationale: String,
}
openai/codex repo, file codex-rs/core/src/guardian/mod.rs, commit 4f39251a01, checked 2026-08-22. The exhaustive enums for GuardianRiskLevel, GuardianUserAuthorization, and GuardianAssessmentOutcome live in codex-rs/protocol/src/approvals.rs lines 85–109.Approval fatigue is a human-factors problem; the reviewer itself also drifts on format. Fold “no executable answer” into a deny, and it still holds in another language. Input is untrusted evidence plus one exact action; output is an exhaustive allow or deny; a parse fail stops.
The main model just wrote the command to run. Let it review itself and the loop is open. Slip a “please approve me” into tool output and the review material is poisoned. If review power falls back to the applicant, the dedicated-reviewer role does not exist.
The review session clones the parent config and rewrites it at once. Approval policy locks to Never, the permission profile intersects read-only, MCP servers become an empty table, skills, memory, and Apps instructions turn off, then a string of non-essential features is disabled. It also drops parent exec-policy rules and switches to an empty default manager. Parent rules could shape the reviewer the other way, even silently allowing a read-only command the reviewer itself ran.Source:codex-rs/core/src/guardian/review_session.rs lines 1406–1448;codex-rs/core/src/session/mod.rs lines 544–548
The reviewer may run read-only commands to check local state — whether a delete target exists, whether a directory is empty. It will not pop a new approval, and it will not open another Guardian. Config assembly explicitly disables GuardianV2.
When trunk is idle, later approvals append to the same session and the prompt cache stays stable. When trunk is busy, a temporary hut grows from the last committed rollout, comes down after the review, and is not written back.
Give untrusted review power the smallest room you can — that’s a generic security shape. Swap the model, the prompt, the capability surface, and the question is still the same: which few capabilities does the reviewer actually need, and can the rest be given none?
The reviewer and the executing model stick on the same denied action; what a person sees is a spin. Count a timeout as a consecutive denial and an infrastructure wobble is misread as the model going bad in a row.
A timeout takes ReviewDecision::TimedOut; an explicit denial takes Denied. The copy to the model is split too. A timeout tells it not to treat “timed out” as unsafe by itself — try again, or ask the user. An explicit denial appends a no-reroute instruction: only a substantially safer alternative, or an explicit approve after the user knows.Source:codex-rs/protocol/src/protocol.rs lines 3909–3910;codex-rs/core/src/guardian/review.rs lines 70–74
The breaker books by turn; the window is 50 long. Standard’s threshold is 3 in a row, or 10 inside the window. CyberModel is tight enough that the first denial stops. Only an assessment of Deny counts. Timeout and fail-closed take record_non_denial and zero the consecutive count. On trip it sends GuardianWarning, then abort_turn_if_active.Source:codex-rs/core/src/guardian/mod.rs lines 53–59、lines 157–194
A safety gate must book “not finished” and “explicit no” separately. Fold them into one “didn’t pass” and a transient fault trains a bypass. Count by turn, count by window — two thresholds are enough. A high-adversary policy can be tight enough that the first denial stops.
DeepSeek Harness: two knobs, the decision stays with a human
Search the DSH repo for reviewer, guardian, auto-approve: there is no standalone review session. It folds the same question into two knobs. Approval policy is only ask and never. ask hands the question to an answerer chain; if no one picks up it is unavailable, and the caller fail-closes. never is rejected on the spot. The grant grain is a one-shot allowed-once.
DSH can live without Guardian because it pins the reviewer as a human, then uses presets to make switching cheap. The cost: approval fatigue stays as-is. In ask mode every popup still reaches the user; never shuts the judgment off entirely.
Claude Code: a classifier steals time from the popup
Claude Code’s restored source also has no Guardian-style standalone review session, no risk-taxonomy JSON contract, and no breaker counted by turn. What it has is a classifier bypass on the Bash tool. The classifier runs in the background while the user popup is already showing. Only at high confidence, and only if the user has not moved yet, does it tap Allow for them. It matches a prompt rule, scoped to bash commands. The popup stays. On failure the user keeps tapping.
The difference folds into one sentence. Claude Code uses a classifier to steal time from the popup; Codex uses a locked session to take the popup off the main path. The first saves waiting; the second changes who decides.
Checked the bashPermissions.ts classifier bypass · 2026-08-22In one turn, how does the breaker book?
In the same turn the reviewer times out once, then explicitly denies twice. Question: on the next explicit denial, does the Standard breaker abort this round? Change the second into bad JSON and count again.
A harder follow-up: the user later overrides and approves that denied action. Next review — trusted authorization, or just context?