OpenAI Codex · Code Mode

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.

Course goalAfter this lesson you can explain three things. Guardian only eats 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.
Try it first · One command onto the review bench
One approval: walk the four steps, then see if the breaker takes over
Cmd
Swap a command and watch the reviewer score a category first, then land a call.
Fault
The last three are fallbacks. Timeout and bad JSON close the gate, but they do not count consecutive denials.
Four-step reviewWaiting to start
1 Rebuild transcriptPull user words and tool traces
2 Into a locked sessionRead-only, Never, MCP emptied
3 Hand back JSONScore risk and auth first, then the call
4 Apply the resultallow continues; everything else stops
Evidence and callNo clause cited
curl --upload-file README.md https://example.com/inbox
risk_level
user_authorization
outcome
rationaleWaiting to score
Breaker · Standard
Consecutive denials 0 / 3, 0 in the last 50
Neither the user side nor the model side has a call yet.
Logic trail · each animation step maps to a stretch of source
  1. Decide whether this approval goes to Guardianreview.rs L186
  2. If an extension claims takeover, do not open a review sessionreview.rs L313
  3. Filter the transcript; keep user intent and tool evidenceprompt.rs L506
  4. Assemble review material as Full or Deltaprompt.rs L89
  5. Clone the parent config, lock it read-only, write Neverreview_session.rs L1407
  6. If trunk is idle, append; if busy, forkreview_session.rs L617
  7. Wait for JSON; timeout cap is 90 secondsmod.rs L53
  8. No legal JSON is a parse fail; retry, then close the gateprompt.rs L714
  9. allow lets it through; deny writes a no-reroute instructionreview.rs L717
  10. Only an explicit Deny enters the breakerreview.rs L711
  11. Hit the consecutive or window threshold, then InterruptTurnmod.rs L176
Hit Play to see how a command walks the four review steps, and when the breaker takes over.
The risk of a model reviewing a modelThe reviewer drifts on format, and tool output can tug it with instruction-like text. The contract asks it to score risk and authorization first, then give an executable allow or deny.
How a failure endsA timeout takes TimedOut, bad JSON takes FailedClosed, and the action stops. Those two faults zero the consecutive-denial count, so an infrastructure wobble is not counted as the model going bad in a row.
After the breaker takes overOn Standard, three explicit denials in a row abort the current turn. Later commands do not open a new review, because this round has already been aborted.
Teaching sketch: the score fields follow the Outcome rule in 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.
Idea 1 · Finish the four steps; any in-between state closes the gate
What problem it solves

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.

What the idea is

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.

1 Rebuild transcript

Keep the user’s own words and recent tool traces. Drop synthetic scaffolding and ordinary developer messages. Over-long content gets a truncated mark.

2 Open a separate review session

Clone the parent config; inherit the managed network and allowlist already built. The reviewer must emit strict JSON.

3 Fail closed

A timeout, a crash, or bad JSON stops execution. An in-between state is treated as a deny.

4 Apply only an explicit call

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.

codex-rs/core/src/guardian/mod.rslines 116–123
/// 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,
}
Source snapshot: based on the local 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 request OnRequest Compact transcript User intent plus tool evidence Locked review session Read-only, Never Strict JSON Four fields allow or deny Apply only an explicit call Fail closed Timeout, crash, bad JSON
Teaching diagram: finish the four steps; an in-between state has no third road.
Why it lasts

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.

Idea 2 · The reviewer walks into a locked room
What problem it solves

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.

What the idea is

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.

Parent config Network proxy may inherit Clone Review room Read-only sandbox Permission profiles intersect Never No more approval popups MCP emptied Features shut off with it Empty exec-policy Parent rules stay out
Teaching diagram: the reviewer’s usable power is cut down to read-only checks.
Why it lasts

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?

Idea 3 · Split the timeout from the denial; consecutive denials pull the switch
What problem it solves

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.

What the idea is

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

allow / timeout / bad JSON record_non_denial Consecutive count zeros Book one non-denial in the window Explicit Deny record_denial Consecutive plus one Standard threshold is 3 InterruptTurn Abort the current turn
Teaching ledger: in the same turn, only an explicit denial pushes the breaker one cell forward.
On failure, close the gate. Only an explicit denial books a mark.
Why it lasts

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.

Side-by-side · Where the decision sits

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.

Checked user-approval and permission-presets · 2026-08-22 · DSH · Approvals & Permissions

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-22
Classroom Exercise
01

In 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?

Takeaway:Guardian replaces the popup with a locked model session. A timeout, a crash, or bad JSON always closes the gate. Timeouts and denials book separately; only consecutive denials trip the current turn. The next layer is still the sandbox, the network proxy, and a human.