OpenAI Codex · Sandbox

Sandbox manager: compile a permission profile into one command

Workspace writable, network tightened. The same git status gets sandbox-exec on Mac, a helper on Linux, and may walk out unchanged on Windows. The difference is the compiler.

Course goalAfter this lesson you can explain how a permission profile is picked as a sandbox type, then compiled into one command. Wanting a sandbox and whether this machine has a backend are two questions. The same profile is also rendered into the environment_context the model sees.
Try it first · One command, three shells
One profile: watch what shell each machine puts on
Windows tier
Off cannot provide an implementation. Turn it on and Windows reaches beat two.
Tool preference
Forbid nails “not needed.” Require nails “needed,” not whether the machine has a backend.
Managed network
Once on, Auto must sandbox. If Windows is still off, the type is still None.
Profile the model sees

Not rendered yet.

Waiting to start. Hit Play to see what shell the same command gets on three machines.
Logic trail · each animation step maps to a source span
  1. Read the PermissionProfile triadmodels.rs L411
  2. Layer on additional permissionspolicy_transforms.rs L525
  3. If Auto, run should_require_platform_sandboxpolicy_transforms.rs L541
  4. select_initial then asks get_platform_sandboxmanager.rs L62
  5. None passes argv through unchangedmanager.rs L366
  6. macOS prefixes /usr/bin/sandbox-execmanager.rs L401
  7. Linux serializes the profile to the helperlandlock.rs L23
  8. Windows beat one leaves argv alonemanager.rs L447
  9. The same profile renders into environment_contextenvironment_context.rs L96
Hit Play to see what shell the same git status gets on three machines.
Three command lines
Need vs can provide
The model’s side
Teaching sketch: the command is fixed as git status; no real shell is invoked. Outer wrapping is simplified to the platform contract. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · Wanting it and having it are two questions
What problem it solves

Same repo, three machines, the model issues the same git status. Config writes the same permission profile: workspace writable, network tightened.

Someone debugging will think the sandbox never took. The profile is still in the logs, and that environment_context is still in the model context. The platform layer just never compiled the profile into a wrapping command. On Windows with the switch off, the compiler hands back the original argv; the policy layer is still there.

What the idea is

SandboxManager asks “do we want it” first, then “do we have it.” should_sandbox returns one boolean. Forbid is always false, Require always true, Auto looks at the profile shape. A managed-network requirement forces it on. When the network is tightened, it goes on unless the caller owns the filesystem. Only when the network is open and the filesystem is unrestricted does it skip.

Source: codex-rs/sandboxing/src/manager.rs lines 310–329 · codex-rs/sandboxing/src/policy_transforms.rs lines 541–561

Then get_platform_sandbox names a type by OS. macOS gets Seatbelt, Linux a seccomp helper. Windows has an extra switch; off means empty. Empty means this host has no implementation to dispatch.

Source: codex-rs/sandboxing/src/manager.rs lines 62–76

select_initial asks the first question, then folds an empty second answer into SandboxType::None. The profile says it needs one; the host cannot provide it; the type is still None.

Source: codex-rs/sandboxing/src/manager.rs lines 293–306

Permission profile Who builds it, how wide the entries Want it? should_sandbox → bool Have it? get_platform_sandbox → optional Three machines’ shells macOS · sandbox-exec Linux · helper plus profile JSON Windows · beat one leaves argv No implementation → keep the original command
Teaching diagram: ask the two questions apart, then the compile product forks by platform.
Why it lasts

Intent in config and the backend on the host will part ways. If product copy, the status bar, and the model brief all read the “we hope it’s on” side of config, debugging starts crooked here. Two functions: one returns a boolean, one an optional backend name. Rewrite it in another language and you still want this.

Idea 2 · One profile, two exits
What problem it solves

If permissions are written twice, you change one side and forget the other, and the model plans the next step against a stale boundary. The model seeing no outer sandbox, and Windows falling to None when the sandbox is off, should be two exits of the same intent.

What the idea is

The three PermissionProfile variants say who builds the outer sandbox. Managed: Codex itself stitches the wrapping command. Disabled: no outer layer. External: the caller owns the filesystem; the network may still belong to Codex.

Source: codex-rs/protocol/src/models.rs lines 411–425

transform dispatches by type. None passes the user argv through and does not even prepare a local cwd, because an unsandboxed request may carry a remote-machine path. macOS trusts only /usr/bin/sandbox-exec; policy goes on -p, paths on -D, the user command after --. Linux serializes the whole profile into --permission-profile; a missing helper errors out and does not degrade to no-sandbox. Windows beat one only validates and leaves argv alone; the wrapper is the current exe, and the shell is deferred to transform_for_direct_spawn.

Source: codex-rs/sandboxing/src/manager.rs lines 365–459 · codex-rs/sandboxing/src/seatbelt.rs lines 52–56 · codex-rs/sandboxing/src/landlock.rs lines 15–23 · codex-rs/sandboxing/src/manager.rs lines 484–518

The same profile is folded by FileSystemContext into an internal enum and rendered into environment_context. Disabled becomes type="disabled" plus an unrestricted filesystem. Model-visible text cannot stop privilege escape; it only cuts wasted attempts.

Source: codex-rs/core/src/context/environment_context.rs lines 96–116

Effective profile Base plus extra transform → argv Isolation happens at the child-process door render → environment_context The model sees the same runtime fact
Teaching diagram: one runtime value, two exits.
When the platform cannot provide an implementation, the XML does not change its story.
Why it lasts

There should be one runtime fact. Isolation happens at the child-process door; the orchestration layer keeps holding paths and the profile, and it lands only at the exec boundary. Platform dialects will change; this profile shape, fed to both the wrapping command and the model, still earns its keep.

Idea 3 · Open with union, approve with intersection
What problem it solves

A single command can also carry an overlay. If a human approval uses union, one mistaken yes writes a path the request never asked for into session grants.

What the idea is

Merge uses union: if either side opens the network, the result is open; filesystem entries are concatenated and deduped. Intersection is the other set: the network stays only if both sides are open; the filesystem keeps only already-approved entries that fall inside the request. An empty intersection is not booked on the session; the base profile stays in force. Empty means this extra opening left no widening. Whether the command can still run is up to later policy and the sandbox.

Source: codex-rs/sandboxing/src/policy_transforms.rs lines 90–142 · codex-rs/sandboxing/src/policy_transforms.rs lines 144–214 · codex-rs/core/src/session/mod.rs lines 2833–2846

Why it lasts

Widening and approval are two jobs. Union makes “this command opened one more path” speakable. Intersection keeps what a human approved no wider than the request. Empty means the opening failed — do not read it as “switch to strictest,” and do not read it as “forbid the whole command.”

Side-by-side · which layer you cut, which way failure leans

DeepSeek Harness: no backend, refuse to run

DSH also cuts at the child-process door. confine picks a runner for the current host, compiles policy into runner args, and puts the user command after --. The profile vocabulary is three file modes; danger-full-access never enters the wrap.

Missing a backend fails closed and does not fall back to the original argv. The copy says refusing to run the command unconfined. Codex also will not run when the Unix helper is missing; when the Windows sandbox is off it folds “cannot provide” into None and hands it to the policy layer.

Source checked on both sides · 2026-08-22 · DSH · Sandbox

Grok: one apply, lock the current process

Grok also has a type named SandboxManager; its job is apply() then install(). The comment is blunt: it acts on the current process, irreversibly. The compile product is a CapabilitySet, not a wrap around each command’s argv.

Unsupported platform, or apply failure: warn, record apply_failed, keep running. Later tool calls share the same capability. Grok does not recompile argv per command, and cannot give two commands two boundaries inside one process.

Source checked on both sides · 2026-08-22 · Grok · Five sandbox Profiles

Source: packages/sandbox/sandbox-local/src/index.ts lines 316–333 · crates/codegen/xai-grok-sandbox/src/lib.rs lines 117–136

Classroom Exercise
01

Four returns, three inputs

Open should_require_platform_sandbox and sketch the four returns on paper. Then predict these three: a Disabled profile plus managed network; writable root plus one deny, network Enabled; an External profile, network Restricted.

Advanced: the same Disabled profile, the tool declares Require, Windows tier still off. What is the type, and will the XML the model sees change its story?

Takeaway: Write “needs isolation” and “what this machine can provide” as two functions. One permission profile feeds both the wrapping command and the model. Windows is two beats; when off it hands back the original argv, and the policy layer is still there.