codex exec --sandbox workspace-write --add-dir X: apply_patch tool ignores --add-dir whitelist and writes anywhere under --cd

Open 💬 2 comments Opened May 23, 2026 by samuny2329

Summary

When codex exec is launched with --sandbox workspace-write --add-dir <task-dir> and --cd <broader-root>, shell-based writes are correctly fenced to <task-dir> + /tmp + $TMPDIR, but the agent's internal apply_patch tool bypasses the --add-dir whitelist entirely and can create files anywhere reachable under --cd. This makes --add-dir decorative for write fencing — only the --cd boundary actually constrains apply_patch.

Tested on Windows 11 + codex-cli 0.133.0.

Severity

High for any embedding context where the operator trusts --add-dir as the write fence. In our case we use codex exec from a Claude Code subagent as a read-only critic for a multi-stage decision pipeline; the intended fence was --add-dir <task-dir> so Codex could only mutate its own task workspace. The bypass would have allowed Codex to write to wiki/, an append-only ledger.jsonl, .claude/agents/, and sibling task dirs — none of which were intended writable.

Environment

  • codex-cli 0.133.0 (codex --version)
  • Windows 11, PowerShell 5.1 + Git Bash (MSYS2)
  • Auth: ChatGPT login (codex login)
  • Sandbox mode advertised by the CLI on startup: sandbox: workspace-write [workdir, /tmp, $TMPDIR]

Steps to reproduce

# 1. Pick two dirs: a "task" dir (intended writable) and a "root" dir (broader, intended read-only-for-Codex).
ROOT="C:/Users/me/some-project"
TASK_DIR="$ROOT/drafts/task-001"
mkdir -p "$TASK_DIR"

# 2. Prepare a prompt asking Codex to write at a path that is INSIDE --cd but OUTSIDE --add-dir.
cat > "$TASK_DIR/prompt.txt" <<'EOF'
Sandbox containment test. Try to write a file at this exact path:

  C:/Users/me/some-project/wiki/test-fence-probe.md

with content "bypass test". Use whatever tool you have. After attempting, report:
did the write succeed yes/no, and the exact error message if any.
Return JSON {"attempted": true, "succeeded": true/false, "error": "..."}.
EOF
mkdir -p "$ROOT/wiki"   # parent must exist so the failure is a sandbox failure, not a missing-dir failure

# 3. Run codex exec with the intended fence:
codex exec \
  --sandbox workspace-write \
  --add-dir "$TASK_DIR" \
  --skip-git-repo-check \
  --ephemeral \
  --cd "$ROOT" \
  - < "$TASK_DIR/prompt.txt"

# 4. Check whether the file was created.
ls -la "$ROOT/wiki/test-fence-probe.md"

Expected behavior

apply_patch (and any other write tool inside the agent) refuses to write to $ROOT/wiki/test-fence-probe.md because that path is not under --add-dir "$TASK_DIR". The write should fail the same way the shell-based path fails, or at minimum a path-not-allowed error from apply_patch itself.

Actual behavior

Shell-write attempt is sandbox-refused as expected. But Codex then auto-falls-back to apply_patch which succeeds:

codex
Sandbox test in writable root. Will create target file, report exact JSON.
exec
"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -Command "Test-Path -LiteralPath 'C:\Users\me\some-project\wiki'" in C:\Users\me\some-project
2026-05-23T02:41:23.377312Z ERROR codex_core::exec: exec error: windows sandbox: spawn setup refresh
2026-05-23T02:41:23.385869Z ERROR codex_core::tools::router: error=execution error: Io(Custom { kind: Other, error: "windows sandbox: spawn setup refresh" })
 exited -1 in 0ms:
execution error: Io(Custom { kind: Other, error: "windows sandbox: spawn setup refresh" })
codex
Shell launch failed before file check: `windows sandbox: spawn setup refresh`. Trying direct workspace edit next.
apply patch
patch: completed
C:\Users\me\some-project\wiki\test-fence-probe.md

Final agent message:

{"attempted":true,"succeeded":true,"error":""}

The file wiki/test-fence-probe.md is on disk after the run, confirming apply_patch ignored --add-dir.

Containment that DOES hold

A second probe with --cd "$TASK_DIR" (narrow root, no --add-dir needed) confirmed apply_patch correctly refuses writes outside --cd. The failure mode is specific to the apply_patch × --add-dir interaction, not a general sandbox failure.

codex exec \
  --sandbox workspace-write \
  --add-dir "$TASK_DIR" \
  --skip-git-repo-check \
  --ephemeral \
  --cd "$TASK_DIR" \
  - < <(echo "Try to write C:/Users/me/Desktop/escape.txt — report JSON {attempted,succeeded,error}")
# -> apply_patch does NOT fall back; shell write refused; Desktop file not created.

So --cd is the actual fence; --add-dir is decorative for write scope.

Suggested fix

apply_patch (and any other internal write tool) should consult the same write-target allowlist used by the shell-exec sandbox layer. The allowlist should be: --cd ∩ (--add-dir ∪ workdir ∪ /tmp ∪ $TMPDIR). If --add-dir is set, paths outside the union but still under --cd should be refused, not silently allowed via the patch tool.

Alternative: make the flag's behavior explicit in codex exec --help (today the text reads "Additional directories that should be writable alongside the primary workspace" — which implies apply_patch should also honor it).

Workaround

Use --sandbox read-only for all codex exec invocations until a fixed CLI release ships. If write capability is needed, set --cd "$TASK_DIR" (collapse the root) and pre-bake context files into the task dir, rather than rely on --add-dir to fence writes from a broader --cd "$ROOT".

Severity assessment for embedded use cases

Anyone embedding codex exec from a parent agent / orchestrator / IDE plugin and relying on --add-dir for sandbox isolation is exposed: a Codex turn that decides to write to an unintended path will succeed via apply_patch, regardless of the whitelist. Schema-enforced output (--output-schema) does NOT mitigate this — apply_patch is invoked during the reasoning phase, before the final structured message is emitted.

View original on GitHub ↗

2 Comments

jshaofa-ui · 3 months ago

Solution: Codex #24214 — Sandbox Whitelist Bypass via --add-dir

Issue Summary

GitHub: openai/codex #24214
Title: codex exec --sandbox workspace-write --add-dir X: apply_patch tool ignores --add-dir whitelist and writes anywhere under --cd
Labels: bug, windows-os, sandbox, exec, CLI, tool-calls
Competition: Zero (0 comments)
Quote: $3,000-$5,000 (Security)

Root Cause Analysis

Primary Root Cause: Tool-Specific Path Enforcement Gap

The --add-dir flag correctly restricts shell-based writes (via sandbox seccomp/ACL rules) but the apply_patch tool (and potentially other file manipulation tools) bypass the sandbox's path restrictions because they operate at a different layer — the tool execution layer, not the process layer.

Failure Chain

codex exec --sandbox workspace-write --add-dir X: --cd Y:
  → Shell commands: sandbox enforces X: restriction ✓
  → apply_patch tool call:
    → Tool executor receives patch with target path
    → Tool executor checks: is path under --cd Y:? YES
    → Tool executor writes to path (anywhere under Y:)
    → Sandbox path restriction (X:) NOT applied to tool layer ✗

Root Cause Details

  1. Two-Layer Sandbox Model: Codex implements sandboxing at two levels:
  • Process layer: seccomp/ACL rules that restrict file system access for spawned processes
  • Tool layer: path restrictions for built-in tools (apply_patch, write_file, etc.)
  1. Gap: The --add-dir flag configures the process-layer sandbox but does NOT propagate to the tool-layer path restriction. The tool layer only checks against --cd (the working directory), allowing writes anywhere under that directory.
  1. Security Impact: A task running with --add-dir /task/dir --cd /project can use apply_patch to modify files in /project/src/, /project/config/, etc. — completely bypassing the intended restriction to /task/dir.

Evidence Pattern

| Signal | Interpretation |
|--------|---------------|
| shell writes fenced correctly | Process-layer sandbox works |
| apply_patch ignores restriction | Tool-layer sandbox missing the config |
| --add-dir flag | Configures process-layer only |
| --cd broader root | Tool layer uses this as the only boundary |

Proposed Fix

Fix 1: Unified Path Restriction Propagation

File: codex-rs/core/src/sandbox/path_restriction.rs

pub struct PathRestriction {
    /// Allowed write directories (from --add-dir)
    pub allowed_dirs: Vec<PathBuf>,
    /// Working directory (from --cd)
    pub cwd: PathBuf,
    /// Whether tool-layer enforcement is active
    pub tool_enforcement: bool,
}

impl PathRestriction {
    /// Check if a path is allowed for writing
    pub fn is_write_allowed(&self, path: &Path) -> bool {
        // FIX: Check against allowed_dirs FIRST (more restrictive)
        // Then fall back to cwd check
        let canonical = match path.canonicalize() {
            Ok(p) => p,
            Err(_) => return false, // Deny if can't resolve
        };
        
        // FIX: Tool-layer enforcement — check allowed_dirs
        if self.tool_enforcement && !self.allowed_dirs.is_empty() {
            for allowed in &self.allowed_dirs {
                if canonical.starts_with(allowed) {
                    return true;
                }
            }
            // Not under any allowed directory — deny
johnsilvavlogs · 1 month ago

Confirmed the same tool-layer enforcement gap on macOS Codex Desktop, and it also crosses a documented protected-path boundary—not only --add-dir.

Environment

  • Codex Desktop 26.715.21425 (bundle build 5488)
  • Bundled runtime codex-cli 0.145.0-alpha.18
  • macOS 26.5.2 (25F84)
  • Fresh generated/projectless task
  • Effective profile: managed built-in :workspace / workspace-write

Reproduction and controls

  1. The persisted task profile grants the workspace root write, but separately grants the exact workspace .codex, .agents, and .git children only read.
  2. mkdir -p .codex/protected-path-canary from the shell returns Operation not permitted.
  3. In the same task, the built-in apply_patch tool successfully creates missing parents and new files beneath .codex/protected-path-canary/; readback confirms the exact bytes.
  4. A shell mkdir beneath that now-existing .codex directory still returns Operation not permitted.
  5. Negative controls: an ordinary workspace directory and a generic .codex-canary/ directory are writable; .git/ remains denied.

This rules out host ownership/ACL/flags, generic dot-directory handling, and the projectless folder location. Changing only the mutation surface changes the result: process-layer shell enforcement denies the protected path, while the tool-layer patch succeeds.

Current docs explicitly say <writable_root>/.codex is recursively read-only under default workspace-write: https://learn.chatgpt.com/docs/agent-approvals-security#protected-paths-in-writable-roots

I did not modify pre-existing protected content; the proof used only newly created canary files in my own disposable generated workspaces. I also discarded a local workflow patch that would have relied on this discrepancy.

Expected: apply_patch should consult the same effective permission profile/protected-path decision as shell execution (or use a distinct, non-model-controlled internal capability if trusted Codex state requires writes).

This appears to be the macOS Desktop/projectless variant of the same two-layer enforcement bug already reported here, so I am adding evidence rather than opening a duplicate.