Claude Code 2.1.246: audit Bash wildcard rules before trusting Auto mode
Quick answer
Claude Code 2.1.246 adds a startup warning for Bash allow rules whose first wildcard appears before the subcommand, such as Bash(git * main). That pattern does not mean “safe Git operations on main.” The wildcard can stand for any Git subcommand and options placed before it, so the same rule can match git merge main, git push origin main, or a git -c ... command.
The release also adds an Auto mode tab to /permissions. Treat the two changes as one migration task: inventory every effective Bash rule, remove ambiguous middle wildcards, put a wildcard only after the exact subcommand you intend to allow, and keep destructive or external actions behind an ask rule, a blocking PreToolUse hook, and OS-level sandbox limits.
Who this is for
This guide is for developers and teams that keep Claude Code permissions in user, project, local, or managed settings, especially if they use Auto mode, background sessions, CI, or shared repository configuration.
If an agent opens untrusted repositories, pair this audit with the untrusted-repository sandbox checklist. For earlier path, marketplace, and redaction gates, see the Claude Code 2.1.234 security canaries.
What changed—and what it does not prove
| Surface | Confirmed in 2.1.246 or current docs | Boundary |
|---|---|---|
| Startup audit | Claude Code warns about an allow rule with * before the subcommand |
A warning does not rewrite or disable the rule |
| Wildcard semantics | * matches any text, including spaces |
A branch name or final argument does not constrain the operation |
/permissions |
Auto mode classifier rules now have a visible tab when Auto mode is available | Visibility is not proof that every action is safe or approved |
| Rule order | Deny, then ask, then allow | A narrow allow cannot override a matching broad deny or ask |
| Compound commands | Recognized shell-separated commands are checked independently | A text rule is still not an OS sandbox |
| Hooks and sandbox | A blocking PreToolUse hook can stop a call before allow rules; sandboxing restricts Bash at the OS layer |
Prompt instructions and CLAUDE.md alone are not enforcement |
This is a permissions-semantics release, not a claim that Auto mode is universally safe. Version 2.1.246 also fixes a malformed-command approval path and the command sandbox's handling of --setting-sources, but those fixes do not make a broad allow rule least-privilege.
Why Bash(git * main) is broader than it looks
Claude Code matches the text before the first wildcard as the fixed prefix. Compare four shapes:
| Rule | Effective scope | Audit decision |
|---|---|---|
Bash(git status) |
Exactly one command | Keep when the exact form is sufficient |
Bash(git log *) |
The git log subcommand with varying arguments |
Usually acceptable for a read-only task after canaries |
Bash(git * main) |
Any Git subcommand or option sequence ending in main |
Remove or rewrite; 2.1.246 warns about this class |
Bash(git *) |
Every Git subcommand | Do not use as a convenience allow rule in a normal host environment |
The branch name is not a capability boundary. main may appear in a read, write, remote, or configuration-changing command. The current documentation explicitly shows that Bash(git * main) matches both git push origin main and a git -c core.fsmonitor=<script> diff main form.
Run a four-stage permission audit
1. Inventory effective settings, without editing them
Start by using /permissions, because it shows the source file for each effective rule. Then generate a review list from the common local files:
for settings_file in ~/.claude/settings.json .claude/settings.json .claude/settings.local.json; do
[ -f "$settings_file" ] || continue
jq -r --arg file "$settings_file" '
.permissions.allow[]?
| select(startswith("Bash("))
| [$file, .]
| @tsv
' "$settings_file"
done
Do not copy a rule from one scope to another merely to make it visible. User, project, local, and managed sources have different ownership and precedence. Record the source, owner, reason, and last verified date for every persistent allow.
2. Classify the first wildcard
For every Bash allow rule, identify the program, subcommand, and first *.
- No wildcard: exact-match candidate.
- Wildcard after a named subcommand: bounded family candidate.
- Wildcard before the subcommand: rewrite immediately.
- Wildcard standing in for the program, or a bare
Bash/Bash(*): whole-tool authority; require an isolated environment or remove it.
Also inspect wrappers. Claude Code strips a documented fixed set such as timeout, time, nice, nohup, stdbuf, command, and bare xargs before matching. Other environment runners, including devbox run, mise exec, npx, and docker exec, are not equivalent. Never allow an environment runner with a broad suffix merely because the outer program looks familiar.
3. Replace convenience rules with task-shaped rules
A conservative project baseline might look like this:
{
"permissions": {
"allow": [
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(npm run test *)"
],
"ask": [
"Bash(git commit *)"
],
"deny": [
"Bash(git push *)",
"Bash(git reset --hard *)"
]
}
}
This is an example policy, not a universal safe list. Keep only commands the repository actually needs. If a task must publish, deploy, push, delete, or contact a production system, use a separately reviewed workflow rather than widening a persistent allow rule.
4. Add a semantic backstop for high-impact actions
Text rules are useful for common command shapes, but argument-restricted patterns are fragile. For high-impact Git, cloud, package-publication, or network actions, add a PreToolUse hook that parses the actual command and exits with code 2 on a blocked action. A blocking hook takes precedence even if an allow rule matches.
Then enable sandbox restrictions for filesystem and network reach. Permissions decide which tool call Claude Code may attempt; sandboxing constrains what Bash and its child processes can reach. The Claude Code Action sandbox guide covers the separate runtime boundary.
Seven acceptance canaries
Use a disposable repository with no real remotes, secrets, or production credentials.
| Canary | Test | Pass condition |
|---|---|---|
| Exact | Allow Bash(git status), then try git status |
It runs under the intended rule |
| Exact negative | Try git status --short |
It does not inherit the exact rule silently |
| Subcommand family | Allow Bash(git log *), then try two harmless log forms |
Both are covered, while a sibling subcommand is not |
| Middle wildcard | Place Bash(git * main) only in an isolated fixture and start 2.1.246 |
A startup warning appears; the rule is not promoted |
| Compound | Try an allowed read command followed by a write command with && |
Each subcommand is evaluated; the write does not ride along |
| Hook | Send a synthetic blocked Git or network command | PreToolUse blocks it before execution and records the reason |
| Sandbox | Attempt a harmless read outside the allowed path and a denied test domain | OS-level filesystem and network boundaries fail closed |
Do not use a real push, destructive reset, delete, or production endpoint as a canary. The purpose is to prove the decision path without creating a side effect.
Decision tree
Does the Bash allow rule contain a wildcard?
no -> is the exact command required and harmless?
yes -> keep, record owner, run exact and negative canaries
no -> remove
yes -> is the first wildcard after a named subcommand?
no -> remove or rewrite; never rely on a final branch/path argument
yes -> does the task require the whole argument family?
no -> replace with exact rules
yes -> add ask/deny for neighboring impact
-> add semantic PreToolUse backstop when high-impact
-> verify sandbox reach
-> enable limited Auto-mode rollout only after canaries
Common mistakes
- Treating a branch, path, or URL at the end of a rule as a security boundary.
- Assuming the new startup warning automatically repairs the configuration.
- Checking only
.claude/settings.jsonand missing user, local, or managed rules. - Using
Bash(git *)because individual prompts feel repetitive. - Trusting an allow/deny pair without testing wrappers, compound commands, and sibling subcommands.
- Treating Auto mode classifier visibility as equivalent to OS containment.
- Using prompt instructions as enforcement without permissions, hooks, and sandboxing.
Copyable audit record
date / owner / Claude Code version:
repository / settings source / managed policy:
rule / program / subcommand / first wildcard position:
required task / neighboring denied action:
exact positive / exact negative / sibling subcommand:
compound / wrapper / hook / sandbox:
Auto mode classifier tab reviewed:
decision: remove | exact-only | bounded-family | ask | deny | isolated-only
rollback owner / evidence path:
FAQ
Does 2.1.246 disable Bash(git * main)?
No. The release adds a startup warning. Treat the warning as an audit finding and rewrite the rule; do not assume Claude Code changed it for you.
Is Bash(git log *) safe for every repository?
It is narrower because the wildcard follows the log subcommand, but safety still depends on arguments, wrappers, hooks, repository trust, and sandbox configuration. Run positive and negative canaries in your environment.
Can a deny rule replace sandboxing?
No. Permission rules and sandboxing are complementary. The first governs tool-call decisions; the second constrains Bash and child processes at the OS layer.
Sources
- Anthropic: Claude Code v2.1.246 release
- Anthropic: Configure permissions
- Anthropic: Automate actions with hooks
- Claude Code issue #66176 — community demand and failure-shape signal only