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 FIRSTWhat 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
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.
Name the input, owner, approval, and recovery action for one automated step.
A successful run that cannot explain what happened or be safely repeated.
- The same argv is handed to both lensesL scene
- Refusable first reads the three Decision states and takes the strictestdecision.rs L9
- The approval-cache key carries the full argv; prefixes do not countunified_exec.rs L92
- Guardian times out or sees bad output and closes the gateguardian/mod.rs L11
- Three platform-sandbox backends; Windows off is Nonemanager.rs L37
- Replayable trusts the JSONL original; SQLite is only a mirrorREADME.md L22
- fork must hit a real TurnStartedthread_rollout_truncation.rs L187
- Failure is written as an observation; success stays truecontext.rs L351
- The proxy 403 goes back to the command process; the loop continuesresponses.rs L80
- Compare the two verdicts: agree or contradictL accept
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?
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.
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.
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?
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
Source: codex-rs/network-proxy/README.md lines 234–238
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
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.
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
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.
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
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
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.
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.
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.
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.
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.
No discussion on this article yet.