Permission-denied message shows a sub-command excerpt of the Bash command, not the full command or the matching deny rule
Summary
When a Bash tool call is blocked by a permissions.deny rule, the message returned to the model isPermission to use Bash with command <text> has been denied., where <text> is frequently an excerpt
of the command that was actually issued (a subset of the sub-commands split on |, &&, ;, newlines),
and the message never names the deny rule that matched.
Two consequences observed over ~3 weeks of daily use (Claude Code 2.1.2xx, macOS, permissions.defaultMode: "auto"):
- In 7 of 10 non-test denials, the displayed command was an excerpt. In 3 of those the excerpt began with
an unrelated sub-command (e.g. head -10 … or tail -3 …) while the sub-command that actually touched the
denied path came later in the pipeline. The model has no way to tell which sub-command triggered the denial.
- In 2 cases the model (and the humans reading the transcript) misattributed the cause — once to "an execution-
environment classifier block", once to a PreToolUse hook — because the message gave no rule name. Both were
later traced to a user-level Read(./<file>) deny rule being cross-applied to Bash.
Environment
- Claude Code 2.1.260 (also reproduced on 2.1.2xx builds from mid-August 2026), macOS (arm64)
- User settings:
permissions.defaultMode: "auto";permissions.denycontainingRead(./.env),Read(./.env.*),
Edit(./.env), Edit(./.env.*) and, from 2026-08-22, Bash(cat *.env*), Bash(grep *.env*), etc.
- No managed settings; project settings contain only
hooks.
Minimal reproduction
- In
~/.claude/settings.jsonadd"permissions": {"deny": ["Read(./.env)"]}and start Claude Code in a project
that contains a .env file.
- Ask Claude to run a multi-stage Bash command whose last stage reads the file, e.g.
sed -n '1,5p' README.md; echo "---"; grep -n "^FOO" .env | sed 's/=.*/=<redacted>/'.
- Observe the tool result:
Permission to use Bash with command grep -n "^FOO" .env has been denied.
— the sed …, echo … stages and the trailing | sed … are dropped, and the rule (Read(./.env)) is not named.
With a longer pipeline the excerpt can start at an unrelated stage.
Expected
- The message should either quote the full command as issued, or clearly mark that it is an excerpt
(e.g. … command (excerpt: sub-command 3 of 4) …).
- The message should name the matching rule and its source file/level
(e.g. denied by Read(./.env) in ~/.claude/settings.json), the same way PreToolUse hooks can
identify themselves in their stderr text.
Additional notes
- The documentation (
/docs/en/permissions) states that Read/Edit deny rules also apply to "file commands Claude Code
recognizes in Bash, such as cat, head, tail, and sed". It does not say whether grep, ls or stat are
recognized. We observed one denial of ls -la .env; stat -f "%Sm" .env at a time when no Bash deny rule existed,
so at least ls/stat appear to be matched by the Read rule, but nothing in the message confirms this.
- Hook-based blocks (
exit 2from a PreToolUse hook) are easy to attribute because the hook's own stderr text is
returned; deny-rule blocks are the only ones that are opaque.