anthropics / anthropics/claude-code

[BUG] 2.1.277: excludedCommands "every part must match" also drops single commands carrying a pre-subcommand flag (git -C, -c, --git-dir)

Open
#95,455 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

api:vertex area:sandbox bug has repro platform:linux platform:wsl regression
Dominant language
TypeScript
Stars
147k
Forks
24k
PR merge metrics
PR metrics pending

Description

Preflight Checklist
  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report
  • I am using the latest version of Claude Code
What's Wrong?

2.1.277 includes this fix:

Fixed a sandbox.excludedCommands glob exempting an entire compound Bash command from the sandbox when only one part matched; every part must now match

That fix is correct and closes a real bypass (#81157). But it appears to over-match: a single command — no pipe, no chain, no redirect, therefore exactly one "part" — is also no longer exempted when it carries a flag before its subcommand.

With sandbox.excludedCommands: ["git *"]:

Command 2.1.276 2.1.277
git status --porcelain excluded excluded
git --no-pager status --porcelain excluded excluded
git --literal-pathspecs status --porcelain excluded excluded
git -C . status --porcelain excluded sandboxed
git -c core.pager=cat status --porcelain excluded sandboxed
git --git-dir=<p>/.git --work-tree=<p> status --porcelain excluded sandboxed
git --exec-path=/usr/lib/git-core status --porcelain excluded sandboxed

Every one of these is a single command and matches the glob git * as a plain string. Bare boolean pre-flags are unaffected; flags that take a value are not.

Impact, in order of severity:

  1. git status returns plausible wrong output instead of failing. It exits 0 and produces well-formed porcelain that happens to be wrong: paths the sandbox masks (home-directory dotfiles such as .bashrc and .gitconfig, plus configured deny-read paths) are reported as untracked repository files. This reads as ordinary working-tree noise. In my case it was dismissed as exactly that, and the underlying condition went undiagnosed across several later commands, including a staging decision. Anything that parses git status is affected. An error would have been strictly safer than a wrong answer.

  2. Credentialed operations fail with a misleading error. git -C <path> fetch / push fail with fatal: could not read Username for '<host>', because the credential helper cannot read its configuration inside the sandbox. The surfaced error points at credentials or networking rather than at sandboxing, which invites misattribution.

  3. A common pattern breaks. git -C <dir> … is a standard way to act on another worktree without changing directory, including inside scripts and tooling.

Why this looks like over-matching rather than intent. If the goal were to exclude git's repo/config-retargeting flags on security grounds — they are genuine arbitrary-code-execution vectors (-c core.sshCommand=…, -c credential.helper=…, --exec-path= into a writable directory) — that would be defensible. But it is not mentioned in the changelog, which describes only the compound-command tightening; it is not in the settings documentation; --exec-path and -C are treated the same as -c while harmless value-less flags pass, a split that tracks "flag takes a value" rather than "flag is dangerous"; and the status failure mode is silent wrong output, which no intentional security control would choose.

What Should Happen?

A single command carrying pre-subcommand flags should be treated as one part, so git -C <path> status continues to match git * and run excluded — exactly as it did in 2.1.276.

If any of these flags are excluded deliberately, it should be documented in the settings reference and should fail loudly rather than returning sandbox-contaminated git status output.

Error Messages/Logs
# Same repo, same cwd, two separate Bash tool calls, 2.1.277.

$ git status --porcelain
# (clean — correct)

$ git -C . status --porcelain
?? .bashrc
?? .gitconfig
?? .profile
?? .zshrc
# …and further entries, one per sandbox-masked path.
# None of these files exist in the repository.

# Credentialed form:
$ git -C /path/to/repo fetch origin main
warning: unable to access '/path/to/repo/.gitmodules': Permission denied
fatal: could not read Username for 'https://<host>': No such device or address

# Same command without -C, immediately afterwards, succeeds:
$ git fetch origin main
From https://<host>/<owner>/<repo>
 * branch            main       -> FETCH_HEAD
Steps to Reproduce
  1. Enable the sandbox and add a git exclusion in ~/.claude/settings.json:

    {
      "sandbox": {
        "enabled": true,
        "excludedCommands": ["git *"]
      }
    }
    
  2. In any git repository, run this as a Bash tool call:

    git status --porcelain
    

    Note the output — it reflects the true working-tree state.

  3. As a separate Bash tool call (chaining would itself defeat the exclusion, correctly, under the 2.1.277 rule), run:

    git -C . status --porcelain
    
  4. Compare. -C . is a no-op, so the two should be identical. On 2.1.277 the second additionally lists sandbox-masked paths as untracked files.

The same split appears with git -c core.pager=cat status, git --git-dir=… --work-tree=… status, and git --exec-path=… status, while git --no-pager status and git --literal-pathspecs status remain correctly excluded.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.276

Claude Code Version

2.1.277 (Claude Code)

Platform

Google Vertex AI

Operating System

Ubuntu/Debian Linux

Terminal/Shell

WSL (Windows Subsystem for Linux)

Additional Information

Not fully deterministic. Across sessions on the same machine and version, the same command shape failed, failed, then succeeded within about 60 seconds. Behaviour was stable and reproducible within a given session but not identical across sessions, which suggests the classification may be cached or computed per session. Flagging it because it makes the bug hard for users to characterise — my own first attempt produced a clean, reproducible-looking rule that did not hold outside that session.

Only tested on Vertex AI. Nothing about this looks provider-specific — it is in the Bash/sandbox layer — but I have not confirmed it on the first-party API or Bedrock.

Workaround. Change directory in its own command, then run plain git:

cd <dir>
git status --porcelain

Note that (cd <dir> && git …) is not a workaround — that is genuinely a compound command, so the 2.1.277 rule correctly declines to exempt it.

Related issues.

  • #81157 — the compound-command bypass that this 2.1.277 change fixes. This report is about that fix over-matching, not a request to revert it.
  • #82109 — excludedCommands skipped when the excluded command is inside a shell loop; same family, command structure defeating the match.
  • #92445 — excludedCommands intermittently proxied; possibly related to the non-determinism above.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in the Bash/sandbox layer and reproduce the settings.json excludedCommands case by comparing plain git status with git -C . status. Trace how pre-subcommand flags are classified versus compound commands. Done means a single flagged git command matches the exclusion while compound commands still require every part to match, with coverage for the reported flag forms.

Written by the indexing model from the issue text.

Assessment

Tech stack
bash, git, python
Domain
cli, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.