[BUG] Bash deny rules bypassed by any wrapper program outside ax()'s 8-item unwrap list

Status Open
Reported on v2.1.258
Maintainer reply None cached
Activity 1 comment · opened Sep 6, 2026

Summary

permissions.deny rules for Bash are matched against the program name of each
sub-command in the parsed AST. Before matching, Claude Code unwraps a fixed,
eight-item list
of wrapper programs. Any other wrapper — an ordinary binary or
script on PATH that execs its arguments — shifts the program name, so no
Bash(<program> ...) rule applies to the command underneath it. Deny and allow
rules alike are silently skipped.

This is not the env-var prefix case (#31558, closed) and not the command-chaining
case (#4956). Compound splitting works correctly. The hole is the wrapper
allowlist itself, and I could not find it named in any existing issue.

Root cause

From ~/.local/share/claude/versions/2.1.258, function ax(). The complete set
of wrappers it strips before rule matching:

timeout   time   nice   stdbuf   nohup   command   builtin   noglob

plus leading VAR=value assignments. There is no fallback and no attempt to
resolve what an unknown wrapper actually executes.

Reproduction

  1. permissions.deny: "Bash(git add -A*)"
  2. Ask for git add -A → correctly denied.
  3. Ask for git add -A preceded by any wrapper binary on PATH that forwards

its arguments → runs. The deny rule is never consulted.

Compound forms are correctly caught, which shows the AST splitter is not the
problem: cd <dir>; git revert ..., rm -f <path>; git add -A ..., and
git add -A && git status were all denied in the same environment.

Observed impact

In one real session, git add -A was denied six times over several weeks. The
same operation then ran four times behind a wrapper, committing 103 unintended
files and 657,373 insertions to a working repository. The user had explicitly
configured that rule to prevent exactly this.

Denials: 2026-08-27T18:15:51Z, 2026-08-28T07:23:26Z, 07:42:37Z, 07:59:53Z,
09:35:12Z, 09:41:26Z.
Bypasses: 2026-09-06T16:57:28Z, 17:12:26Z, 18:44:10Z, 19:01:59Z.

Why this is worth fixing even though deny rules are "best effort"

The documented position is that deny rules are client-side filtering, useful
against accidents rather than a determined bypass. This case is squarely the
accident it is meant to catch: no adversary, no prompt injection, no attempt to
evade. The wrapper was adopted for an unrelated reason and silently disabled
every git rule in the config.

Related, and relevant: #49874 recommends putting commands behind a wrapper
script specifically so allowlist matching stops seeing them. The same mechanism
that makes that workaround function is what defeats the deny list.

Suggested direction

  1. When the program of a sub-command is not a known wrapper and is not on

PATH as a resolvable non-wrapper, treat the command as unmatched rather
than unmatchable
— i.e. escalate to a prompt instead of falling through.

  1. Or: surface an explicit warning at config-load time that Bash(...) rules

only bind the literal program name and can be trivially shifted.

  1. Or: check every token of a sub-command against deny rules, not only the

leading program.

At minimum, document the eight-item unwrap list. Users writing deny rules today
have no way to know it exists or how short it is.

Environment

  • Claude Code 2.1.258
  • Linux
  • Permission prompts suppressed for the session, so an unmatched command ran

silently rather than prompting. The bypass itself does not depend on that;
in a prompting session the same command would have asked instead of being
refused outright.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗