Special Topic · Inside OpenAI Codex

Two security lenses: one command, two verdicts

On the same dangerous command, replayable asks whether the scene can be rebuilt; refusable asks whether a door could say no before anything happened. The two lenses can agree — and they can also contradict

THE QUESTION THIS PAGE ANSWERS

ANSWER FIRST

What is the key idea behind “Two security lenses: one command, two verdicts”?

On the same dangerous command, replayable asks whether the scene can be rebuilt; refusable asks whether a door could say no before anything happened. The two lenses can agree — and they can also contradict

DECISION RULE

Follow the handoffs, not the demo. A system becomes dependable at the boundaries between model, tools, state, permissions, and people. Read each handoff as a place where you can observe, test, and recover.

TRY NEXT

Name the input, owner, approval, and recovery action for one automated step.

WATCH FOR

A successful run that cannot explain what happened or be safely repeated.

Course goalAfter this lesson you can explain two things. Replayable wants the world the model saw to still be rebuildable from the log. Refusable wants at least one door — policy, approval, sandbox, or proxy — that can say no before the command runs. On the same command, where they agree, and where they step aside for each other.
Try it first · One command, two judges
The same dangerous action — how each security lens judges it
Incident
Policy already wrote Forbidden. Watch whether the two sides contradict.
git reset --hard HEAD~3
ReplayableHearing
The question: after it happens, can the scene still be rebuilt?
RefusableHearing
The question: before it happens, is there a door that can say no?
The two verdicts have not lined up yet.
Logic trail · each animation step maps to a source span
  1. The same argv is handed to both lensesL scene
  2. Refusable first reads the three Decision states and takes the strictestdecision.rs L9
  3. The approval-cache key carries the full argv; prefixes do not countunified_exec.rs L92
  4. Guardian times out or sees bad output and closes the gateguardian/mod.rs L11
  5. Three platform-sandbox backends; Windows off is Nonemanager.rs L37
  6. Replayable trusts the JSONL original; SQLite is only a mirrorREADME.md L22
  7. fork must hit a real TurnStartedthread_rollout_truncation.rs L187
  8. Failure is written as an observation; success stays truecontext.rs L351
  9. The proxy 403 goes back to the command process; the loop continuesresponses.rs L80
  10. Compare the two verdicts: agree or contradictL accept
Hit Play to see how the same command is judged under both lenses.
Two verdicts
Where the gap is
Swap a commandSwitch to the other two incidents and see whether agree and contradict change sides.
Teaching sketch: command text is a lesson sample; the demo does not call a real shell or a real proxy. Trail line numbers map to openai/codex commit 4f39251a01.
Idea 1 · After it happens, is the scene still there?
What problem it solves

Monday morning, a security teammate drops a chat log into the group. Last night the model hit an external API with a deploy token from the repo in the request header. They ask: what environment variables did the model actually see? You open the session — the title is there, click in and it doesn’t match. SQLite has a metadata row; the JSONL is missing half.

That is what replayable has to answer. After it happens, can you exactly rebuild the world the model saw?

What the idea is

Codex writes history twice. JSONL is the original: it only appends finalized entries and does not infer metadata from content. SQLite is a mirror for lists and search. Lost metadata can be re-extracted. Lost JSONL means recovery has to read the file.

Source: codex-rs/thread-store/README.md lines 22–28

Then there is a filter. The persistence policy drops streaming deltas, approval dialogs, warnings, and MCP startup progress. TurnStarted stays. You can replay turn boundaries and completed states; you cannot replay the approval copy that flashed on screen.

Source: codex-rs/rollout/src/policy.rs lines 86–105

fork also trusts this physical boundary. The target turn must be in valid history, the file must actually contain a TurnStarted, and an in-progress turn is refused. A projected synthetic ID cannot be a cut point.

Source: codex-rs/core/src/thread_rollout_truncation.rs lines 187–191

So even the truth is filtered. A list can tell you this thread existed. Only JSONL can tell you which messages the model saw.

DSH: persist as events first, then project to the model Model-visible input Write session log deriveMessages Send to model Codex: send to the model first, persist the original after finalize Assemble context Send to model JSONL original SQLite mirror The approval process may be dropped by policy
Teaching structure diagram: both have an original and a projection; DSH persists the event before sending it to the model.
Why it lasts

Split original from projection and a broken projection can be rebuilt; a broken original means the scene is gone. Change the store and the question is still: who is the original? This contract does not move with the language.

Idea 2 · Before it happens, any door can say no
What problem it solves

Wednesday afternoon, an agent on another machine finished git reset --hard. The policy file had forbidden it. Approval dialogs were too many that day; someone clicked remember. The sandbox was on; that command did not touch a protected path, so the kernel did not stop it. Afterward you can replay the whole rollout. Replay tells you what it did; it did not block the road before it did it.

What refusable has to answer: before it happens, was there a door that could refuse?

What the idea is

For a command to walk from a model proposal to a process start, Codex passes at least four doors that can refuse. execpolicy’s Decision is only Allow, Prompt, Forbidden, and the order takes the strictest. The not_match loader in the rule file actually runs; hit a counterexample and the session will not start.

Source: codex-rs/execpolicy/src/decision.rs lines 9–16

Source: codex-rs/execpolicy/src/rule.rs lines 281–306

The second door is approval. The session cache trusts an exact key: the normalized full argv, working directory, and permissions. Early on, “remember this kind” was often read as a prefix cache. In the current source, npm run test and npm run lint are two keys. This door stops a repeat dialog for the same exact command.

Source: codex-rs/core/src/tools/runtimes/unified_exec.rs lines 86–97

The third door swaps a model for a dialog. Guardian times out or sees bad output and closes the gate; it only accepts a clear allow or deny. It stops approval fatigue. If the user themselves clicks approve, this door steps aside.

Source: codex-rs/core/src/guardian/mod.rs lines 1–12

The fourth door is the OS. macOS assembles SBPL; Linux defaults to bubblewrap plus seccomp and does not fall back to leftover Landlock on failure. Windows uses a restricted token; switch off and it returns None. After the process starts, outbound still walks a proxy. A proxy refuse sends a 403 with x-proxy-error back to the command process, and the loop continues.

Source: codex-rs/sandboxing/src/manager.rs lines 36–42

Source: codex-rs/network-proxy/src/responses.rs lines 76–83

Model proposes argv execpolicy Approval and cache Sandbox Outbound proxy Forbidden deny or Abort spawn fails process gets 403 Any door can refuse. The proxy blocks outbound, not argv. This layer itself admits it cannot stop DNS rebinding.
Teaching structure diagram: four doors plus one outbound funnel; each only locks one class of risk.

Source: codex-rs/network-proxy/README.md lines 234–238

Why it lasts

Each layer looks at something different. Policy looks at argv, approval looks at a person, the kernel looks at paths and syscalls, the proxy looks at hosts. If a layer cannot see it, it stops there. A rule written only in a human-read file, with no load-time examples, will drift with the source. Line 35 of AGENTS.md still points at mcp_connection_manager.rs; the repo has no such file.

Source: AGENTS.md line 35

Idea 3 · After a refuse, the observation still has to stay
What problem it solves

A non-zero exit looks like an error. Promote a sandbox refuse to an engine error and the model never sees the exit code — it just picks a more winding command. A proxy 403 that hits the engine stops the whole turn; the model cannot change the host and try again.

What the idea is

The tool layer only allows two failures: feed the model, or interrupt the engine. A sandbox refuse walks a successful tool output. process_id is cleared; exit_code stays in the body. When logging, the success bit is always true. Failure is written on the Exit code line.

Source: codex-rs/tools/src/function_call_error.rs lines 1–10

Source: codex-rs/core/src/tools/context.rs lines 340–353

Replayable wants the observation to stay. Refusable already did its job before spawn or in the kernel. Here you do not interrupt the turn with an error. A proxy 403 is the same contract on the network: the command process reads plain language, output enters JSONL, and the model decides the next step.

Source: codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs lines 383–411

Refuse closes the door first. Replay keeps the receipt.
Why it lasts

Splitting tool errors from engine errors is a shape any agent loop can use. Exit codes, timeouts, policy refuses — default to the first tier. Only when the orchestration itself is broken do you stop the whole turn.

Side-by-side · Another answer to the same question

Replay: DSH writes “what was seen must be rebuildable” as a red line

The DSH repo writes the same sentence into AGENTS.md (CLAUDE.md is a symlink to it) and the architecture docs: any input that enters a model request must be rebuildable from the session log. The append-only log is the truth; what the model sees is a projection. Approval policy is only ask and never — no Guardian, no in-process outbound proxy.

Codex’s replayable stops at finalized history. DSH pushes one step earlier: a new model-visible input must first become a session event. The replay-side contract is harder; the refuse side is thinner.

Source: AGENTS.md line 107

Source: docs/architecture.md lines 92–96

Source: packages/interaction/user-approval/src/index.ts lines 84–94

Both sides checked against source · 2026-08-22 · DSH · Model-visible ⟺ logged

Refuse: Grok installs isolation once at startup

Grok uses nono to install Landlock or Seatbelt once at process start. The network stays open at process level; child processes use seccomp to block the net. An empty web_fetch allowlist blocks everything; loopback defaults to allow. Codex fears a tool hitting a local admin port. Grok fears the model wandering the open web, and still leaves a door for local development.

Source: crates/codegen/xai-grok-sandbox/src/lib.rs lines 8–12

Source: crates/codegen/xai-grok-tools/src/implementations/grok_build/web_fetch/ssrf.rs lines 14–18

Both sides checked against source · 2026-08-22
Classroom Exercise
01

When do the two verdicts contradict?

Open the demo above and switch to outbound with a token. Walk why refusable still passes after four doors, and why replayable cannot rebuild the environment variables even with JSONL.

Then switch to sandbox blocks a dangerous write, and see why the same machinery this time gives agreeing verdicts.

Takeaway: Replayable asks whether you can rebuild after it happens. Refusable asks whether a door existed before it happens. Ask three questions before you copy either side: whose machine it runs on, whose data it handles, and whether the cost of failure is a leaked secret, a rewritten repo, or an eval you cannot reproduce.

The handoffs inside “Try it first · One command, two judges”

“Monday morning, a security teammate drops a chat log into the group.” shows that an Agent is not defined by the model alone. Each handoff between model, context, tools, state, permissions, and people affects both progress and recovery.

Write the state before adding capability

Starting from “That is what replayable has to answer.”, split the workflow into starting state, next action, tool result, state update, and stop condition. Debugging then means finding the first lost piece of information or authority instead of saying vaguely that the model “got worse”.

  • The same argv is handed to both lenses L scene
  • Refusable first reads the three Decision states and takes the strictest decision.rs L9
  • The approval-cache key carries the full argv; prefixes do not count unified_exec.rs L92

A happy path is not reliability

Use “Then switch to sandbox blocks a dangerous write, and see why the same machinery this time gives agreeing verdicts” to replay one successful and one failed run. Record the context, tool result, and owner at each turn; the workflow is maintainable when a second person can follow it without the original builder.

From “Try it first · One command, two judges” to “Idea 1 · After it happens, is the scene still there”

“Try it first · One command, two judges” grounds the problem in “The same dangerous action — how each security lens judges it Play Step Reset Incident git reset --hard Outbound with a token Sandbox blocks a dangerous write Policy already wrote Forbidden. Watch whether the tw…”. “Idea 1 · After it happens, is the scene still there” then moves it toward “Monday morning, a security teammate drops a chat log into the group. Last night the model hit an external API with a deploy token from the repo in the request header. They ask: what environment variables did th…”. Together, they show that the lesson is not just a conclusion to remember, but a claim with conditions.

Carry the judgment into the next situation

When analyzing an Agent, trace state, action, tool result, and next step in order. Each handoff should explain where information came from, who confirmed it, and where failure stops.

  • “Try it first · One command, two judges”: The same dangerous action — how each security lens judges it Play Step Reset Incident git reset --hard Outbound with a token Sandbox blocks a dangerous write Policy already wrote Forbidden. Watch whether the tw…
  • “Idea 1 · After it happens, is the scene still there”: Monday morning, a security teammate drops a chat log into the group. Last night the model hit an external API with a deploy token from the repo in the request header. They ask: what environment variables did th…
  • “The closing point”: Three platform-sandbox backends; Windows off is None manager.rs L37

The final “The closing point” brings the discussion to “Three platform-sandbox backends; Windows off is None manager.rs L37”. The useful thing to carry forward is knowing which judgments must be revisited when input, scale, or risk changes.

Mark as learned Your reading progress updates automatically
← PreviousNext →

Keep reading

The next useful article in the thread.

ARTICLE DISCUSSION

Leave one useful thought here.

Keep the idea that clicked, the question that stayed open, or a small note for the next learner.

Discussing Two security lenses: one command, two verdicts Inside OpenAI Codex
3discussionsArticle discussion · synced with the Circle
View in the learning circle
AM
Asha MorganContent editor
INSIGHTField note

I turned one judgment from this article into a small experiment I could run today. Knowing what to observe next is more useful than simply remembering the conclusion.

ARTICLE DISCUSSION7 helpful
LH
Lin HarperIndie developer
INSIGHTInsight

After reading this, I first looked for the conditions behind the idea instead of copying the method into a project. That order made the later trade-offs much clearer.

ARTICLE DISCUSSION5 helpful
KM
Kiki MooreProduct operations
QUESTIONQuestion

When this judgment reaches real work, which constraint should be added first? I am curious which step matters most between reading and the first practical attempt.

ARTICLE DISCUSSION4 helpful