← All posts

Guide

Codex CLI sandbox modes: read-only, workspace-write, and when each is right

Codex CLI sandbox modes: read-only, workspace-write, and when each is right

Codex CLI has a feature almost nothing else in the terminal-agent category has: a sandbox the operating system enforces. Not a system prompt asking the model to be careful, not a regex over proposed commands — an Apple Seatbelt profile on macOS and a bubblewrap container on Linux, so a write the policy forbids fails with EPERM whether or not the model intended it.

It is also one of the worst-explained features in the category. The flag names have changed, one of them was removed, the mode you get by default depends on a decision you made weeks ago in a different folder, and half the internet conflates the sandbox with the approval prompt. This is the guide.

Verified against: @openai/codex 0.146.0 (npm latest, tagged rust-v0.146.0, released 2026-07-29), checked on 2026-08-02 against the source in github.com/openai/codex and the docs at developers.openai.com/codex (which now redirect to learn.chatgpt.com/docs). Everything below names the file it came from. This part of Codex moves fast — re-check flag names against codex --help before you paste anything into CI.

Two dials, not one

The single most common mistake is treating "sandbox" and "approvals" as one setting. They are two, and they answer different questions:

Dial 1 — sandbox kernel-enforced read-only workspace-write danger-full-access what a command CAN do blocked Dial 2 — approvals interactive only untrusted on-request never what happens when it wants more prompt you or hard-refuse never → refuse
The sandbox stops the command. The approval policy decides whether stopping it means asking you or telling the model no.

The three modes

The values live in SandboxMode in codex-rs/protocol/src/config_types.rs, and the CLI mirrors them one-to-one in codex-rs/utils/cli/src/sandbox_mode_cli_arg.rs:

ModeFilesystem writesNetworkRunning processes
read-only None. Every write path fails at the syscall. Off by default. Allowed. The agent can run ls, rg, git log, a test runner — anything that does not write.
workspace-write Writable roots only: your cwd, plus TMPDIR and /tmp on Unix, plus anything in writable_roots. Off by default. network_access = true turns it on. Allowed.
danger-full-access Everything. No sandbox is applied. On. Allowed.

Two details worth pinning down, because the prose docs blur them.

Read-only does not mean "cannot run commands." The Seatbelt profile Codex ships (codex-rs/sandboxing/src/seatbelt_base_policy.sbpl) opens with (deny default) and then explicitly grants (allow process-exec) and (allow process-fork), with the note that "child processes inherit the policy of their parent." A read-only Codex is a Codex that can explore your repo at full speed and cannot leave a mark on it. That distinction is the whole point of the mode.

Workspace-write is not "the whole workspace." Under a writable root, Codex keeps three metadata directories protected: .git, .agents and .codex (PROTECTED_METADATA_PATH_NAMES in codex-rs/protocol/src/permissions.rs). The reason is escalation, not tidiness — a writable .git/hooks is a writable arbitrary-code-execution path. On Linux those subpaths are re-applied as read-only bind mounts after the workspace is bound writable.

How the enforcement actually works, per OS

Codex picks a backend by host OS in get_platform_sandbox() (codex-rs/sandboxing/src/manager.rs), and the three arms are not equivalent.

macOS — Apple Seatbelt

Each sandboxed command is wrapped in a Seatbelt profile (the crate calls it SandboxType::MacosSeatbelt). The base policy is closed-by-default and modelled on Chrome's renderer sandbox — the file cites Chromium's common.sb as its inspiration. Reads of platform paths, PTYs, a small allowlist of sysctl names and /dev/null writes are granted back; everything else is denied unless the policy adds it. Network is a separate overlay profile (seatbelt_network_policy.sbpl) that is only concatenated when network access is enabled. This works out of the box on any Mac — nothing to install.

Linux — bubblewrap plus seccomp (Landlock is now the legacy path)

This is the part most write-ups get wrong, including ones published this year. Codex used to enforce the Linux sandbox with Landlock LSM rules plus a seccomp filter. As of the current release, bubblewrap is the default filesystem sandbox and Landlock is an opt-in fallback. From codex-rs/linux-sandbox/README.md:

The practical consequences: WSL2 works, WSL1 does not (it cannot create the required user namespaces, and Codex rejects sandboxed shell commands rather than running them unprotected), and a hardened container that forbids user namespaces will trip the same warning. If you run Codex inside Docker, check that before you assume the sandbox is on.

Windows — off unless you turn it on

Windows now has a native backend, SandboxType::WindowsRestrictedToken, but WindowsSandboxLevel defaults to Disabled and is gated behind a feature flag or an explicit config key (codex-rs/core/src/windows_sandbox.rs resolves it from the [windows] table, sandbox = "unelevated" or "elevated").

The behaviour when it is off is the single most important platform difference in this post, and it is in codex-rs/config/src/config_toml.rs: on Windows with the sandbox disabled, a resolved mode of workspace-write is silently downgraded to read-only. Codex would rather be uselessly safe than pretend. If you are on Windows and wondering why the agent keeps refusing to write, this is why — and the honest fix is WSL2 or the opt-in native sandbox, not danger-full-access.

The second dial: approval policy

AskForApproval (codex-rs/protocol/src/protocol.rs) has these values:

ValueWhat it does
untrustedOnly "known safe" read-only commands auto-run. Everything else asks you.
on-requestThe default. The agent works inside the sandbox and asks when it wants to step outside.
neverNo prompts, ever. A blocked action returns its failure straight to the model.
granularA struct, not a CLI value: per-category booleans (sandbox_approval, rules, skill_approval) where false auto-rejects instead of prompting.

Two things here contradict what you will read elsewhere. on-failure is no longer a distinct policy — it survives only as a serde alias that deserialises to on-request, so an old approval_policy = "on-failure" in your config.toml loads without complaint and silently means something else. And granular is not selectable from the command line; the --ask-for-approval enum exposes exactly three values.

The combination is what decides your day. From codex-rs/core/src/safety.rs, for a patch that wants to write outside the writable roots:

SandboxApprovalResult when the agent tries to write
read-onlyneverHard refusal: "writing is blocked by read-only sandbox; rejected by user approval settings". The model reads that and adapts.
read-onlyon-requestYou get a prompt. Say yes and the write happens outside the sandbox.
workspace-writeneverWrites inside the roots go through silently. Writes outside are refused.
workspace-writeuntrustedEvery non-trivial command asks first, even the ones the sandbox would have allowed.
danger-full-accessneverNothing stops anything. Only defensible when something else is the sandbox.

Note the asymmetry: untrusted makes an already-safe sandbox noisier, not safer. If you want quiet safety, the answer is a tighter sandbox with never, not a louder approval policy.

The default is not a constant

If you set nothing, what do you get? The resolution logic in ConfigToml::derive_permission_profile is, in its own comment: "If no sandbox_mode is set but this directory has a trust decision, default to workspace-write except on unsandboxed Windows where we default to read-only." Otherwise it falls through to SandboxMode::default(), which is read-only.

In plain terms: a folder you have already answered the trust prompt for gets workspace-write; a folder you have not gets read-only. Two clones of the same repo in two paths can behave differently. This is fine interactively and unacceptable in a script. Always pass --sandbox explicitly in anything automated.

codex exec versus the TUI

codex exec is the non-interactive entry point: it takes a prompt as an argument or on stdin, streams progress to stderr, and puts the final answer on stdout. The permission surface is deliberately narrower than the TUI's, and the difference is not cosmetic.

Which gives the combination you want whenever a script, a CI job, or another tool is driving Codex: read-only plus non-interactive. The sandbox guarantees the run cannot mutate anything; never guarantees it cannot stall waiting for a keystroke nobody will press. Failure becomes a non-zero exit and a message, which is what automation can actually handle.

Four recipes

1 — Review a diff without letting the agent touch anything

codex exec --sandbox read-only \
  "Review the uncommitted changes. Flag correctness bugs and missing tests. Do not suggest style nits."

Codex ships a purpose-built subcommand for this too, and it accepts the same --sandbox:

codex exec --sandbox read-only review --base main
codex exec --sandbox read-only review --uncommitted
codex exec --sandbox read-only review --commit a1b2c3d

The agent can run git diff, open every file the diff touches, grep for callers and run a read-only test listing. It cannot "helpfully" apply the fix it just described. For a review pass that is a feature, not a limitation — a reviewer that edits is no longer a reviewer.

2 — Let it write in the workspace, but never reach the network

This is the everyday local setting, and it is already the default for workspace-writenetwork_access is false unless you say otherwise. Make it explicit in ~/.codex/config.toml:

sandbox_mode = "workspace-write"
approval_policy = "on-request"

[sandbox_workspace_write]
network_access = false
writable_roots = ["/Users/you/scratch"]
exclude_tmpdir_env_var = false
exclude_slash_tmp = false

An agent that can edit your repo but cannot open a socket cannot exfiltrate anything it reads, and cannot install a package because a stale error message suggested it. The cost is real: no npm install, no pip install, no git fetch. Codex sets CODEX_SANDBOX_NETWORK_DISABLED in the child environment when network is restricted (and CODEX_SANDBOX=seatbelt on macOS), so your test suite can branch on it and skip the network-dependent cases instead of failing them.

If you need to widen the write boundary for one run rather than forever, --add-dir takes an extra writable directory without editing config.

3 — Running it in CI

CODEX_API_KEY="$OPENAI_API_KEY" codex exec \
  --sandbox read-only \
  --skip-git-repo-check \
  --json \
  -o /tmp/codex-verdict.md \
  "Summarise the risk in this PR. Reply with the single word OK if there is none."

The pieces: --json emits JSON Lines so a downstream step can parse events rather than scrape prose; -o / --output-last-message writes just the final message to a file; --skip-git-repo-check matters because Codex otherwise insists on being in a repo, and some runners check out into odd shapes. --ephemeral keeps session files off the runner's disk entirely.

Do not reach for --dangerously-bypass-approvals-and-sandbox as the CI default. Its own help text says it is "intended solely for running in environments that are externally sandboxed" — if your job already runs in a throwaway container with no credentials worth stealing, that is a defensible use. A self-hosted runner with a cached npm token is not.

4 — Check what the sandbox actually does before you trust it

codex sandbox -- touch /tmp/probe
codex sandbox -- curl -sS https://example.com

codex sandbox ("Run commands within a Codex-provided sandbox") runs an arbitrary command under the same backend Codex would use, choosing Seatbelt, the Linux helper or the Windows restricted token by host OS. On macOS, --log-denials streams the sandbox denials that the command triggered and prints them after exit — the fastest way to find out which path your build tool wanted that the policy said no to. -P / --permission-profile selects a named profile from your config so you can probe the exact policy a session will run under.

Why a read-only agent is worth more than it sounds

We build on this at Backgrind, so this is direct experience rather than speculation. A read-only Codex is exactly what makes it safe to hand the same hard question to several agents at once and compare what comes back. Each one can explore the real repository — read the actual code, run git log, trace the actual call graph — and none of them can change it while doing so. You get four grounded opinions and one unchanged working tree.

Without an OS-enforced read-only mode this is not a thing you can do. Two agents editing the same files concurrently produce a mess neither of them can explain, so the alternative is either running them one at a time or giving each its own worktree and reconciling afterwards. Read-only removes the conflict entirely: exploring a repo is the one operation that parallelises for free. That is the mechanism behind our Fusion council, which convenes Codex alongside Claude Code, Cursor and Gemini — every seat headless and read-only in your repo, one chairman folding the answers into a single verdict.

What to actually set

New to Codex? Start with setting up the CLI. Weighing it against the alternative? Claude Code vs Codex CLI covers the rest of the comparison — the sandbox is the strongest single argument on Codex's side of it.

Frequently asked questions

What are the Codex CLI sandbox modes?

Three: read-only, workspace-write and danger-full-access, selected with --sandbox / -s or sandbox_mode in ~/.codex/config.toml. Read-only still lets the agent run processes; it just cannot write, and has no network by default.

Is the sandbox enforced by the OS or just by the model?

By the OS on macOS and Linux — an Apple Seatbelt profile that starts from (deny default), or a bubblewrap helper that binds / read-only and adds a seccomp network filter. On Windows the native restricted-token sandbox is off by default, and Codex downgrades workspace-write to read-only rather than pretend.

Sandbox mode vs approval policy — what is the difference?

Sandbox mode is what a command can physically do. Approval policy is what happens when the agent wants more: prompt you (untrusted, on-request) or refuse and return the failure to the model (never). Independent dials; the combination is what you feel.

Does codex exec have --ask-for-approval?

No. As of 0.146.0 that flag is defined only on the interactive codex command. codex exec sets the policy to never for you. Your only permission dial in exec is --sandbox.

What is the default sandbox mode?

It varies by folder: read-only where Codex has no recorded trust decision, workspace-write where it does. codex exec is documented as read-only by default. Pass --sandbox explicitly in anything automated.

Whatever happened to --full-auto?

Removed. It no longer parses on the top-level codex command, and on codex exec it survives only as a hidden flag that warns you to use --sandbox workspace-write instead.