excludedCommands glob matching unsandboxes entire shell invocation, enabling sandbox escape via command chaining
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:
- Exact match (e.g.,
"xcodebuild") — only matches the command with zero arguments, making it effectively useless for any real command invocation. Users cannot usexcodebuild -project ... buildwith this syntax.
- 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
- Parse shell structure before matching — split compound commands on
&&,;,||,|and only unsandbox segments that matchexcludedCommands. Non-matching segments should run sandboxed.
- 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(labeledarea:security) - #10524 —
excludedCommandsnot respected on first attempt - #31551 —
excludedCommandsshould 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
This issue has 8 comments on GitHub. Read the full discussion on GitHub ↗