Repository navigation
[BUG] sandbox.excludedCommands can't unsandbox an excluded tool in chained or cd calls, so git breaks under the sandbox #103677
Description
Preflight Checklist
- I have searched existing issues and this hasn't been reported yet
- This is a single bug report
What's Wrong?
With "sandbox": { "enabled": true, "excludedCommands": ["git *", "gh *"] } in ~/.claude/settings.json, git still runs sandboxed in most of the shapes the model actually emits, so git breaks (GPG commit signing, fetch/push over HTTPS). There is no setting that makes an excluded tool run unsandboxed in every call shape.
Measured on 2.1.286, macOS (Seatbelt), desktop app Code tab, auto mode:
| Call (one Bash tool call) | Result |
|---|---|
git ls-remote origin HEAD |
unsandboxed, works |
git -C . ls-remote origin HEAD |
sandboxed: deny network-outbound github.com:443 |
git --git-dir=<repo>/.git ls-remote origin HEAD |
sandboxed |
GIT_DIR=<repo>/.git git ls-remote origin HEAD |
sandboxed |
git -C <worktree> commit -F msg.txt |
sandboxed: gpg: can't connect to the keyboxd: Operation not permitted, commit fails |
cd <repo> && git fetch origin main |
sandboxed (documented) |
The -C / --git-dir part is #95455. This issue is about the rest: under the 2.1.277 "every part must match" rule, any cd, && chain, pipe, loop, $(...) or redirect makes the whole call sandboxed, even though the excluded tool itself must run unsandboxed to work.
Impact:
- Agents and subagents working across git worktrees naturally use
git -C <path>orcd <path> && git .... Every one of those calls fails, and the model then either asks the user to run the command or retries withdangerouslyDisableSandbox(which auto mode's classifier can refuse). - Users are pushed to disable the sandbox entirely (several comments on [BUG] 2.1.277: excludedCommands "every part must match" also drops single commands carrying a pre-subcommand flag (git -C, -c, --git-dir) #95455 say so), which is worse than any per-command exemption.
What Should Happen?
An excluded command should run unsandboxed whatever call shape it appears in. Two possible designs, either would solve it:
- Per-segment sandboxing: parse the call into its segments (Claude Code already does this for permission rules and for the "every part must match" check) and run only the segments matching
excludedCommandsunsandboxed, while the remaining segments stay sandboxed.cd <dir>segments would set the working directory for the next segment rather than force sandboxing. - Opt-in setting, e.g.
"sandbox": { "excludedCommandsMatch": "any" }: if any segment of a call matchesexcludedCommands, run the whole call unsandboxed (the pre-2.1.277 behaviour). It is off by default, so excludedCommands with a glob unsandboxes the entire Bash invocation, including commands that run before the excluded one #81157 stays fixed for users who don't opt in.
Separately, as far as I can tell from the docs, a PreToolUse hook cannot choose sandboxed vs unsandboxed per call, so users can't build either behaviour themselves. Allowing a hook to set that per call would also work.
Steps to Reproduce
~/.claude/settings.json:{"sandbox": {"enabled": true, "excludedCommands": ["git *"]}}- In a git repo, ask Claude to run, as separate Bash calls:
git ls-remote origin HEAD→ worksgit -C . ls-remote origin HEAD→deny network-outbound github.com:443cd . && git ls-remote origin HEAD→ sandboxed (documented behaviour)
- With GPG commit signing configured (
commit.gpgsign=true, keyboxd), rungit -C <repo> commit -m test→gpg: can't connect to the keyboxd: Operation not permitted.
Claude Code Version
2.1.286 (Claude Code)
Operating System
macOS
Terminal/Shell
Claude desktop app (Code tab), zsh
Additional Information
Related: #95455 (pre-subcommand flags), #95532 (redirection defeats exclusion), #81157 (the bypass the 2.1.277 change fixed).
Activity