Skip to content

Course · Codex · Approvals & sandbox

The two-axis model - approval policy is independent of the sandbox

Picture a small, ordinary hardening job in a Python repo: a money amount is converted through a float (int(float(row["amount"]) * 100)) and silently drops a cent on inputs like 0.29. The fix means reading four or five files along the money path, editing two of them, pinning the case with a test, and running the test suite. Nothing exotic - but before you let an agent execute it, you have to decide what you are letting it do.

Your instinct on a job like this is to reach for some “let it run” setting. Before you do, know what you’re actually turning up, because in Codex that instinct splits into two separate questions - and conflating them is how people end up either babysitting work that’s safe or walking away from work that isn’t.

Think about what could go wrong when an agent works on code you care about, and you’ll notice the risks aren’t all the same kind:

  • It could do something you didn’t expect - rewrite a file you meant to keep, run a command that wrecks the working tree. The fence decides how far that mistake can physically reach: inside workspace-write, the blast radius is the workspace and nowhere else.
  • It could need to get beyond the fence - write outside the project, open a network connection. Whether that crossing stops for your y/n is a separate question entirely.

One dial draws the fence; the other staffs the crossings. A single trust slider can’t separate them. “More autonomous” on a one-dial tool means both more-without-asking and more-reach, bolted together - so to stop being interrupted on a safe job, you’d also have to widen the blast radius. That’s a bad trade, and it’s the trade Codex refuses to make you. It gives you two dials.

Before the flags, calibrate the judgment itself. Any autonomy decision rests on two properties of the task, not of the tool: how recoverable the worst single action is, and how far the consequences could reach. The dial below asks exactly those two questions in tool-neutral vocabulary - the worst action runs from a git checkout you can undo to a change no one can, the reach from your working copy to beyond the machine - and answers with one rung of a trust ladder: read and propose only, ask before acting, auto-apply edits with the rest gated, run free inside a fence, or run free with none:

Two questions decide how much leash a task earns - neither of them is “how hard is it.” Set both for the work in front of you and read the rung it lands on.

If the agent’s worst single action went wrong, undoing it would take…
…and its consequences would reach
  1. Run free, no fencedisposable environments onlyNothing pauses it and nothing contains it. No combination on this dial lands here - it belongs only where the whole environment is disposable: a throwaway container, an already-isolated CI runner.
  2. Run free inside a fencethis taskNo prompts; the boundary does the protecting. The agent grinds end to end inside a sandbox, container, or scratch worktree, and you review the whole batch once at the end.
  3. Auto-apply edits, gate the rest
  4. Ask before acting
  5. Read & propose only

A mechanical rename across your own repo is the canonical case: the worst outcome is a git diff you throw away. Prompting on every one of twenty-four identical edits doesn’t add safety - it teaches you to stop reading prompts, which is where real risk starts. Let it run inside the fence and review the batch once.

The rung names are generic on purpose - every tool spells its own versions of them, and most let you set different rungs for different categories of action. The judgment underneath is the same two questions, asked per task, never answered once for all time.

Notice what that ladder is not: two separate settings. It collapses the two questions into a single posture - which is precisely the single-slider shape this section argued against. Collapsing is fine for judging a task; it is a poor shape for configuring an agent, because the interruptions you want and the fence you want won’t always sit on the same rung. Codex’s answer is to split the posture back into two settings you set yourself - one for how much flows without asking, one for how far the agent can reach. (In the ladder’s terms, “run free inside a fence” is the never × workspace-write pairing and “read and propose only” is read-only; the middle rungs that mix the two questions - ask before acting, auto-apply with the rest gated - are postures whole-policy Codex expresses less cleanly, which is roughly what the granular approval form exists for. The exact definitions come next.)

Axis one - how much it can do without asking

Section titled “Axis one - how much it can do without asking”

The approval policy controls when Codex pauses to get your y/n before it acts. You set it with --ask-for-approval (short form -a), and it takes two values:

-a on-request work inside the sandbox without asking; pause when an action
needs to cross the sandbox boundary
-a never never pause - run end to end without interrupting you

The whole first axis runs from “ask me at the fence line” to “don’t interrupt me at all.” In config, approval_policy takes the same two values plus a granular table - { granular = { ... } } - a config-only form that sets approval per action category; the approvals lesson shows when that earns its keep. Notice what the axis does not say anything about: where the agent may write, or whether it may open a network connection. Approval is purely about the interruptions.

The sandbox sets the agent’s default reach: what the execution environment lets it do without an approval-mediated exception. You set it with --sandbox (short form -s), and it takes three values:

-s read-only reads flow; edits and commands that write need approval
(or fail)
-s workspace-write read and write inside the workspace by default; writes
outside it and network access need approval (or fail)
-s danger-full-access no fence at all - whole filesystem, full network

The sandbox is enforced by the execution environment, not by the agent’s good behavior, but it is not an absolute wall: an interactive approval can allow an operation beyond the default boundary. That’s why the two axes are still useful: the sandbox defines the capability boundary, while approval decides whether an exceptional request is allowed. For a non-mutating unattended audit, pair read-only with approval_policy = "never" so no escalation can be approved. The sandbox draws the fence; approval decides who gets to open a gate in it.

Now put a number on the trade, not just a feeling. Take the hardening job as fixed: read the four files along the money path to confirm it (app/importers/csv.py, app/models.py, app/main.py, app/services/ledger.py); edit csv.py to replace the float parse; edit tests/test_import.py to pin the 0.29 case; run pytest. Five files, two edits, one command. Read the two definitions above against that fixed shape and the stop count for each pairing falls straight out - no run required, by construction.

Action Crosses the workspace-write fence? read-only + on-request workspace-write + on-request
Read the four money-path files No - reads need no writes flows flows
Edit app/importers/csv.py (drop the float parse) No - inside the workspace stops - edits need approval flows
Edit tests/test_import.py (pin 0.29) No - inside the workspace stops - edits need approval flows
Run pytest (writes cache files) No - the writes land inside the workspace stops if the cache is needed - see below flows
Total 2 stops for certain, up to 3 0 stops

The rule predicts the two edits exactly: an edit is a write by nature, so under read-only it cannot land without crossing the boundary, and it stops. The test run deserves its asterisk. Under read-only, a command whose writes are optional - and pytest tolerates an unwritable cache directory with a warning - may never trigger a stop at all; the stop happens when the agent asks to cross, not when a write would merely be nice to have. The count is derived from the definitions, not measured: read it as two certain stops plus one conditional, and check it against a real run of your own tooling.

Where does on-request land on its own? Wherever the boundary puts it. A job that stays inside the fence costs zero stops at on-request because nothing it does crosses the fence, not because the agent decided its edits were safe. A job that needs the network stops once per crossing you allow. You’re not buying the agent’s judgment about which edits deserve a second look; you’re buying a line the agent cannot move.

never changes the question. Nothing stops anywhere - so the count that matters becomes blocked versus flowing. Under read-only + never the job is dead on arrival: both edits are refused outright (there is no checkpoint to ask at), and while a cache-tolerant command might limp along with warnings, the job as defined - two edits plus a passing test run - cannot complete. Under workspace-write + never the edits and the test run flow and the job finishes unattended; had anything needed the network or a directory outside the workspace, it would fail, not prompt.

Call this the two-axis model: interruption counted separately from reach, on purpose. A single fused dial could never keep those two counts apart - loosen the slider enough to skip babysitting two mechanical edits and a test run, and you’ve loosened the thing bolted to it too: the reach the sandbox is guarding. The certain-stops-versus-zero column above shows what the sandbox axis alone buys; the never paragraph shows the approval axis moving on its own. Together, they are why this needed to be two dials, not one.

Because the two are orthogonal - each axis moves independently of the other - you pick one value from each, and the pair defines the session. The whole model in one grid - approval policy down the left, sandbox across the top, each cell the session that pairing gives you:

┌─────────────┬──────────────────────┬──────────────────────┬──────────────────────┐
│ approval │ read-only │ workspace-write │ danger-full-access │
├─────────────┼──────────────────────┼──────────────────────┼──────────────────────┤
│ on-request │ read and propose; │ the daily driver │ the fence is gone - │
│ │ every edit and │ (the Auto preset) │ almost nothing left │
│ │ writing command asks │ │ to ask about │
├─────────────┼──────────────────────┼──────────────────────┼──────────────────────┤
│ never │ unattended audit - │ hands-off inside │ no guardrails at all │
│ │ reads only, fails │ the fence; anything │ (--yolo lives here) │
│ │ shut │ outside it fails │ │
└─────────────┴──────────────────────┴──────────────────────┴──────────────────────┘

The grid answers two questions separately. The cell that fits the hardening is on-request × workspace-write: edits and commands inside the workspace flow, and the only stops are boundary crossings. That’s the cell where the job above runs with zero stops - not because the risk disappeared, but because the wall around it is doing the work the interruptions would otherwise have to do. If the work must never escalate, take never with the tightest sandbox that still permits the task; “fails shut” in the grid means errors instead of prompts, not a paused session. (The --yolo shorthand in the bottom-right cell is a single flag that throws both dials fully loose at once; the profiles lesson covers it and why to treat it as a warning sign.)

Two things worth nailing down now, before you start reaching for these flags on real work - both are objections that would otherwise catch you mid-task:

  • The flags are not the whole UX. The CLI exposes the two axes as --ask-for-approval and --sandbox; Codex surfaces also offer named postures - the Auto preset among them, which is exactly workspace-write + on-request. Treat surface-specific names as version-dependent; the two values underneath are the stable part. Legacy flags like the deprecated --full-auto should not be the basis of a new workflow.
  • The default is trust-aware, not one cell. Launch Codex with neither flag and it may start read-only until you explicitly trust the working directory; our recommendation for version-controlled work is the Auto preset - the middle cell of the grid. You loosen on purpose, per task, not by accident.

Neither of those is the same claim as “the default is safe because Codex understands your repo.” It isn’t, and it doesn’t. Two well-set dials only decide how much damage a wrong guess can do while Codex is still guessing - they don’t stop it from guessing wrong in the first place. Both dials are control, one of the course’s four principles: what guides execution and enforces boundaries. The fence bounds what the agent may do; it says nothing about what it knows, which is context, and later chapters take up that half.

You’ll often set both dials persistently rather than typing flags every time - the config keys are approval_policy and sandbox_mode in config.toml, taking the same values (approval_policy adds the granular table). We’ll bundle them into a named profile in the last lesson. The authoritative value lists live in the Codex config reference - the lists above were checked against it in October 2026 - and pinning to the reference beats memorizing, since the options grow over time. (The stop counts in this lesson are derived from the definitions, not measured; the file and edit counts describe the stated example job.)

Now take each axis one at a time. Start with the wall, because it’s the one doing the enforcing - the three sandbox levels in depth.