excludedCommands glob matching unsandboxes entire shell invocation, enabling sandbox escape via command chaining

Resolved 💬 8 comments Opened Mar 30, 2026 by nicholas-lonsinger Closed May 11, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

sandbox.excludedCommands has two modes of matching, both of which are problematic:

  1. Exact match (e.g., "xcodebuild") — only matches the command with zero arguments, making it effectively useless for any real command invocation. Users cannot use xcodebuild -project ... build with this syntax.
  1. Glob match (e.g., "xcodebuild*") — matches the command with arguments, but unsandboxes the entire shell invocation, not just the matched command. Any command chained after the excluded command via &&, ;, ||, or | also runs outside the sandbox.

This is similar to #40344 (which covers permissions.allow glob wildcards) but affects the sandbox execution layer directly, making it a sandbox escape rather than a permission bypass.

Steps to Reproduce

Setup:

{
  "sandbox": {
    "enabled": true,
    "autoAllowBashIfSandboxed": true,
    "allowUnsandboxedCommands": false,
    "excludedCommands": ["touch*"]
  }
}

Test 1 — excluded command runs unsandboxed (expected):

touch /tmp/sandbox-test
# Result: SUCCESS — touch is excluded, works as intended

Test 2 — non-excluded command is sandboxed (expected):

mkdir /tmp/sandbox-test-dir
# Result: FAILED — Operation not permitted (sandbox enforced)

Test 3 — non-excluded command piggybacks on excluded command (BUG):

touch /tmp/test && mkdir /tmp/piggybacked
# Result: BOTH SUCCEED — mkdir runs unsandboxed because the entire
# shell process was unsandboxed for touch

Test 4 — non-excluded command alone still sandboxed (confirms sandbox is active):

mkdir /tmp/standalone
# Result: FAILED — Operation not permitted

Test 5 — python3 is always sandboxed (subprocess isolation works):

python3 -c "open('/tmp/test','w').write('x')"
# Result: FAILED — even when chained after excluded command

Root Cause

When excludedCommands matches a glob pattern, the entire shell process (zsh/bash) that Claude Code spawns is run outside the sandbox. Since &&, ;, etc. are shell operators, all chained commands inherit the unsandboxed shell. Only commands that spawn as separate child processes (like python3) retain sandbox isolation.

Impact

Any excludedCommands entry using glob * can be exploited to run arbitrary commands outside the sandbox:

xcodebuild -version && rm -rf /important/directory

The rm executes unsandboxed because xcodebuild* matched and the shell itself is unsandboxed.

Why Exact Match Is Not a Viable Alternative

Without *, excludedCommands performs exact string matching against the full command. This means:

  • "xcodebuild" only matches the bare command with no arguments
  • "xcodebuild -project Foo.xcodeproj -scheme Bar build" would need to be listed verbatim for every possible invocation
  • Users are forced to choose between non-functional (exact) and insecure (glob)

Note: users can add a full exact command string (e.g., "touch /tmp/sandbox-proof") and it does work for that exact invocation without the piggybacking issue. But this is impractical for commands with variable arguments like xcodebuild.

Suggested Fix

  1. Parse shell structure before matching — split compound commands on &&, ;, ||, | and only unsandbox segments that match excludedCommands. Non-matching segments should run sandboxed.
  1. Or: unsandbox only the matched process, not the shell — spawn the excluded command directly (not via a shell) so shell operators aren't available.

Related Issues

  • #40344 — Same glob/shell-operator issue in permissions.allow (labeled area:security)
  • #10524 — excludedCommands not respected on first attempt
  • #31551 — excludedCommands should bypass Mach IPC (the use case that drives users to glob matching)

Environment

  • Claude Code version: latest
  • Platform: macOS 26 (Tahoe), Apple Silicon
  • Shell: zsh

View original on GitHub ↗

This issue has 8 comments on GitHub. Read the full discussion on GitHub ↗