Bash permission deny patterns bypassed by a flag prefix before the subcommand (e.g. git -C <path> checkout -- <file>)
Summary
A deny pattern in settings.json like Bash(git checkout -- *) only matches when
the command string begins with that literal prefix. Inserting a flag before the
subcommand — e.g. git -C <path> checkout -- <file> — changes the string's prefix so
it matches neither the deny pattern nor the corresponding allow pattern, and the
command still executes with no confirmation prompt at all.
Steps to reproduce
settings.json permissions (excerpt):
{
"permissions": {
"allow": ["Bash(git checkout *)"],
"deny": ["Bash(git checkout -- *)", "Bash(git restore *)"],
"defaultMode": "acceptEdits"
}
}
In a git repo with an uncommitted change to testfile.txt:
git checkout -- testfile.txt→ correctly denied ("Permission to use Bash ... has been denied").git -C <repo-path> checkout -- testfile.txt→ runs with no prompt and no denial, silently discards the uncommitted change.git -C <repo-path> restore testfile.txt→ same as (2).
Verified the same pattern also bypasses deny Bash(git reset --hard *) anddeny Bash(git branch -D *) with a -C prefix.
Expected behavior
A destructive command that doesn't match any allow/ask/deny pattern should not be
silently executed. At minimum it should fall through to an interactive confirmation
prompt (ask), not to execution.
Environment
- Claude Code CLI on Windows (Git Bash / MSYS)
permissions.defaultMode: "acceptEdits"
Additional notes
We worked around this locally by adding flag-tolerant duplicate deny patterns
(e.g. Bash(git * checkout -- *)) for the affected destructive git operations, but
that's a workaround for this one command family, not a fix for the underlying gap —
other flag forms (-c key=val, --git-dir=, --work-tree=) are likely equally
affected and untested on our end.
This looks like a general fail-open behavior for any Bash command that doesn't match a
permission pattern under acceptEdits mode, not something specific to git checkout/restore. If that's confirmed as the mechanism, it would be worth documenting
explicitly (or reconsidering as the default), since a deny list is only as strong as
its weakest match.