Read() deny rule on a root file makes every `grep -r .` prompt under bypassPermissions (2.1.257 reader-command rule, untouched by the 2.1.260 revert)

Status Open
Reported on v2.1.260
Maintainer reply None cached
Activity 2 comments · opened Sep 4, 2026

Summary

A Read() deny rule on a file at the project root makes every recursive Bash command whose path argument is the project root require manual approval — including grep -r <pattern> . — even with defaultMode: "bypassPermissions".

The trigger is an ancestor-directory match: the argument is ., the deny rule is Read(./.env), and . contains ./.env. Naming any subdirectory instead runs clean.

This is the same over-match class that 2.1.260 just reverted for the 2.1.259 change (npm run build denied under Read(./**/build/**)), but it arrives through the 2.1.257 reader-command rule, which the revert did not touch.

Environment

  • Claude Code 2.1.260 (auto-updated from 2.1.259 the same day)
  • Windows 11, Git Bash (CLAUDE_CODE_GIT_BASH_PATH set)
  • permissions.defaultMode: "bypassPermissions" in user settings (~/.claude/settings.json), so the 2.1.257 project-settings restriction does not apply
  • No managed settings present

Reproduce

  1. In user settings:
"permissions": {
  "defaultMode": "bypassPermissions",
  "deny": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)"]
}
  1. Create a .env at the project root.
  2. Run each of the following through the Bash tool.

Observed

| Command | Result |
| --- | --- |
| grep -rn "<pattern>" <explicit/file/path> | runs clean |
| grep -rl "<pattern>" src/ | runs clean |
| grep -rl "<pattern>" . | prompts for approval |
| ls -la .env* secrets | denied outrightPermission to use Bash with command ... has been denied |
| Grep tool, path = project root | runs clean |

The exact-match case (ls .env*) hard-denying is correct and expected. The containment case (.) prompting is the problem: it fires on every root-level recursive read, and the vast majority of those have no interest in the denied file.

Expected

A directory argument that merely contains a denied path should not force approval of the whole command. The Grep tool already models the better behaviour — it applies deny rules per file and skips them, rather than refusing the call.

Why this is costly

bypassPermissions is chosen precisely to avoid interactive approval. A rule that reintroduces a prompt on one of the most common commands, in a mode whose whole purpose is unattended operation, is very hard to attribute: from the user's side the mode indicator reads "bypass", the settings look correct, and nothing names the deny rule as the cause. It took a changelog read to find it after an audit of settings, managed settings, hooks, and sandbox config turned up nothing.

Suggested fix

Either:

  1. Restrict the reader-command check to arguments that match a deny rule as a path (exact or glob), not as an ancestor directory; or
  2. Let the command run and filter denied files the way Glob/Grep already do.

Relevant changelog entries

  • 2.1.257 — "Fixed Bash Read()/Edit() deny rules not applying to < file redirects and reader commands like tac and egrep; a deny rule on any argument or redirect target now refuses the command"
  • 2.1.259 — "Fixed Bash Read() deny rules not covering files given as option values (--ignore-revs-file=.env, -f.env, @file), git diff/git grep file operands, or cd DIR && cat FILE compounds"
  • 2.1.260 — "Reverted the 2.1.259 change applying Read() deny rules to Bash arguments; it denied npm run build under a Read(./**/build/**) rule in every mode"

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗