Read() deny rules force manual approval on unrelated Bash grep commands after cd with relative glob
Description
When a Read() deny rule is configured in settings.json (e.g. to protect .env/.npmrc files), Claude Code prompts for manual approval on any Bash command that does cd <dir> && grep ... <relative-glob>, even when the grep target obviously cannot match the denied pattern.
Example
Settings:
"deny": [
"Edit(*.env*)",
"Read(*.env)",
"Edit(*.npmrc*)",
"Read(*.npmrc)"
]
Command that triggers an unexpected prompt:
cd /Users/me/project/docs
grep -rniI "pattern" open_questions/*.md research/*.md 2>/dev/null | grep -i foo
The tool-permission UI explains: "grep on 'open_questions/.md' after a cd would search a directory that cannot be determined here, and a Read() deny rule is configured; only you can approve running it anyway."*
Expected behavior
Since the glob (open_questions/*.md, research/*.md) cannot possibly resolve to *.env or *.npmrc, this command should run without a prompt — the deny rule is unrelated to the target files.
Actual behavior
Any Read() deny rule anywhere in settings, combined with a cd preceding a relative-path glob in a Bash file-reading command (grep/cat/head/tail/sed), forces a manual approval prompt, regardless of whether the glob could ever match a denied path. There is also no way to write an allow rule (e.g. Bash(grep *)) that is guaranteed to suppress just this class of prompt while keeping deny rules in effect — behavior seems to require accepting the prompt or restructuring commands to use absolute paths.
Environment
- Claude Code version: 2.1.259
- OS: macOS 26.3 (BuildVersion 25D125)
Impact
This makes it impractical to keep narrow, useful Read() deny rules (protecting secrets like .env/.npmrc) without incurring frequent unrelated permission prompts on ordinary read-only search commands run via cd + relative globs, including in automated background workflows where no one is present to approve.
3 Comments
Same regression here on Linux + VS Code extension 2.1.259, permission mode
auto. Adding a few data points that narrow it down.Environment
grepcall (cd DIR; grep ... | head,grep ... | sed -n,grep -r --include ...,pgrep ... | grep -v,... | grep -i ... | head); the same shapes never prompted on 2.1.258.~/.claude/settings.json:defaultMode: "auto",deny: ["Read(.env)", "Read(.env.production)", "Read(.env.staging)", "Edit(.env)", ...]. Noaskrules, no hooks, no managed settings.What the logs show
The prompted calls never reach the auto-mode classifier: the extension log (
Claude VSCode.log) has no[auto-mode] new action being classifiedentry for them, onlyexecutePermissionRequestHooks called for tool: Bashfollowed by the dialog, and laterSlow permission decision: 87515ms for Bash (mode=auto, behavior=allow)when the user clicks. Commands with an explicit subdirectory and nocdare auto-allowed in ~10 ms, so the gate is in the static Bash path check, not in the classifier.Two triggers, both gated on "any
Read(...)deny rule exists"cd DIRfollowed by a relative path argument togrep/egrep/fgrep/rg/diff/git/cp/mv→"<cmd> on '<path>' after a cd would search a directory that cannot be determined here, and a Read() deny rule is configured; only you can approve running it anyway". Thecdis only consolidated when the whole command iscd DIR && ...(no;, no||, no newline, no bare&). Withcd /abs/path; grep -n x rel/file.php | headthe target is a literal absolute path inside the working directory and it still prompts. Your example (cd on one line, grep on the next) is the newline case.cd:grep -r ... .orgrep -rwith no path (implicit.) from the directory that holds.env→"<cmd> on '.' would read '.env', which the deny rule Read(.env) covers; only you can approve running it anyway". That is the #91778 / #91795 case.The result is
behavior: "ask"withclassifierApprovable: false, which is why auto mode cannot rescue it. Neither message string exists in the 2.1.258 binary (0 hits); both are present in 2.1.259.Examples that prompted (repo root as cwd, all read-only)
grep -rn "token" --include=*.php src lib tests(nocd, explicit dirs) was auto-allowed.Workaround Pinned the VS Code extension to 2.1.258; removing the
Read(.env*)deny rules also makes the prompts disappear but drops the protection, so not an option.Same regression on MacOs 27. I'm currently reverting to 258, this is the command if anyone wants it:
Apologies for being unable to post with my enterprise managed user account, which means I can't share any diagnostic data, but consider this a "me too" comment. I typically run with bypassPermissions as the default because of the amount of subagents.
My workflow is typically at a directory level above all my checked out repos. I have ~30 microservices (Java, Spring Boot), 2 UIs (one nx monorepo and typescript and one javascript), and several operations-type repos (actions, cdk8s, terraform). Working at a directory above means that the model (Sonnet 5 mostly) will chain a "cd ..." into one of the sub directories to perform whatever function it needs, such as grep.
Sonnet itself suggested I install a custom hook since hooks are processed before deny() and read() functions. I don't agree with that. My assumption here would be that if I am stating I want to bypass permissions, that bypass should include the new rule that was put in as part of 2.1.259. Hopefully that was the overall intent.
Cheers!