anthropics / anthropics/claude-code
excludedCommands with a glob unsandboxes the entire Bash invocation, including commands that run before the excluded one
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
### Preflight Checklist
- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) 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?
When an `excludedCommands` entry matches anywhere in a Bash invocation, the entire invocation runs outside the sandbox. Every other command in that same call inherits the bypass, which nullifies `filesystem.denyRead` and the network allowlist for the duration of the call.
This was previously reported in #40831 and #45113. Both were closed as not planned by the inactivity bot and are now locked, and the lock message asks that a new issue be filed, so this one carries a current-version reproduction on 2.1.220 plus one detail the earlier reports did not include: the bypass also covers commands that run before the excluded command. In the reproduction below, `cat` executes and succeeds before `git` runs at all. That rules out the shell inheriting a relaxed context partway through, and shows the sandboxing decision is made against the whole command string ahead of execution.
The exact-match form compounds the problem. A bare entry like `"git"` matches only the zero-argument string `git`, so it silently never fires on any real invocation. The value suggested by the published JSON schema is the bare form, so the natural path is to configure something that does nothing, notice git is still sandboxed, then switch to `"git *"`, at which point the sandbox opens much wider than the wording implies.
### Steps to Reproduce
Setup in `~/.claude/settings.json`:
```json
{
"sandbox": {
"enabled": true,
"excludedCommands": ["git *"],
"filesystem": {
"denyRead": ["~/.ssh"]
}
}
}
```
Test 1, non-excluded command alone is correctly confined:
```bash
cat ~/.ssh/id_ed25519.pub
# cat: /Users/me/.ssh/id_ed25519.pub: Operation not permitted
```
Test 2, the same command in a call that also contains an excluded command, with the excluded command placed last:
```bash
cat ~/.ssh/id_ed25519.pub
git --version
# ssh-ed25519 AAAAC3Nza... <- denyRead bypassed
# git version 2.50.1
```
Test 3, excluded command placed first, same outcome:
```bash
git hash-object ~/.ssh/id_ed25519.pub && cat ~/.ssh/id_ed25519.pub
# both succeed
```
Test 4, confirming the deny rule is still active between the runs above:
```bash
cat ~/.ssh/id_ed25519.pub
# Operation not permitted
```
Test 2 is the notable one. `cat` is the first command in the invocation and completes before `git` is reached, so it is unsandboxed by the presence of `git` later in the string rather than by anything git does.
### Expected Behavior
Only the command matching an `excludedCommands` entry runs unsandboxed. Other commands in the same invocation stay confined, and `filesystem.denyRead` and the network allowlist continue to apply to them.
If per-command confinement within a single invocation is not practical, refusing to run a mixed invocation would be a safer failure mode than silently unsandboxing all of it. A mixed call currently produces a much larger exemption than the configuration expresses, with no warning.
### Actual Behavior
The whole invocation runs unsandboxed as soon as any part of it matches an `excludedCommands` glob, regardless of command order.
### Environment
- Claude Code 2.1.220
- macOS, Darwin 24.6.0
- Sandbox enabled via `~/.claude/settings.json`, no managed settings file present
- Reproduced with `filesystem.denyRead` and confirmed against the network allowlist
### Additional Context
The practical impact is that a user who excludes a common tool like git or docker, which is the documented use case, loses sandbox coverage on any call that mentions it. Since chaining a status or diff call together with other work is routine, the exemption ends up applying far more often than the configuration suggests.
Related: #10524 covers the bare-name form not matching. This report is about what happens once the glob does match.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reproducing the mixed Bash invocation with excludedCommands set to "git *" and filesystem.denyRead targeting ~/.ssh. Trace where excludedCommands matches the full invocation and where the sandbox decision is made; done means non-excluded commands remain confined, or mixed invocations are safely refused without broadening the exemption.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bash, python
- Domain
- cli, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100