Deny rules with root-level /** patterns silently match nothing
Bug
Permission deny rules where the relative pattern is only /** (after root resolution) silently fail to match any path. This affects patterns like:
Read(~/**)Read(//**)Edit(~/**)
These rules appear to deny all reads/edits under ~/ or /, but actually deny nothing.
Root cause
In matchingRuleForInput (src/utils/permissions/filesystem.ts:980-984), patterns ending with /** have that suffix stripped before being passed to the ignore library:
if (adjustedPattern.endsWith('/**')) {
adjustedPattern = adjustedPattern.slice(0, -3)
}
This optimization works for patterns like foo/** → foo (the ignore library treats a directory name as matching the directory and its contents). But for root-level patterns:
~/**→patternWithRootproducesrelativePattern = "/**",root = homedir/**stripped of/**→""(empty string)- The
ignorelibrary with an empty pattern matches nothing
Same issue for // prefix: //** → root /, relativePattern /** → stripped to "".
Impact
Users who set "deny": ["Read(~/**)"] expecting to block all reads under their home directory get no protection. The rule is silently accepted and silently ignored. This is a security footgun — users configure deny rules expecting enforcement, get none.
Reproduction
- Add
"deny": ["Read(~/**)"]to~/.claude/settings.json - Use the Read tool to read any file under
~/ - Expected: hard deny
- Actual: read succeeds without prompting
Workaround: use Read(//home/username/**) with the full absolute path (double-slash prefix). This produces a non-empty relative pattern after stripping.
Suggested fix
Handle the degenerate case where stripping /** produces an empty string. For example, use ** as the pattern instead of "", or skip the stripping when the pattern is exactly /**.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗