[Bug] Allow rules under `~/.claude/` show as loaded per /permissions but don't match at runtime
Summary
Allow rules in ~/.claude/settings.json that target paths under ~/.claude/
itself appear in /permissions as loaded but never match at runtime. Edits
to files in any subdirectory under ~/.claude/ prompt for permission
despite a matching rule being present and active.
Tested all four combinations of {tilde, absolute path} × {**/*.md glob,
literal path} for both Edit(...) and Update(...) scope-name forms —
none matched.
Reproducer
- Create a markdown file at
~/.claude/rules/test-rule.md(therules/
subdirectory is the documented standard location for user rule files).
- Add to
~/.claude/settings.json:
``json``
{
"permissions": {
"allow": [
"Edit(~/.claude/**/*.md)"
]
}
}
- Restart Claude Code, or run
/hooksto force-reload settings.
- Verify with
/permissionsthat the rule appears in the loaded allow list.
- Ask Claude Code to edit
~/.claude/rules/test-rule.md.
Expected (per docs): the Edit operation proceeds without a permission
prompt. The rule's gitignore-style ** matches the subdirectory, and the
docs explicitly state *"Edit rules apply to all built-in tools that edit
files."*
Actual: a permission prompt fires titledUpdate(~/.claude/rules/test-rule.md). The rule is bypassed.
What was tested
Each rule below was added to ~/.claude/settings.json, settings reloaded
via /hooks, and /permissions confirmed each rule was loaded. None
matched the runtime edit attempt.
| Rule | Match? |
| ---------------------------------------------------------- | ------ |
| Edit(~/.claude/**/*.md) | No |
| Edit(//<absolute home>/.claude/**/*.md) | No |
| Update(~/.claude/**/*.md) | No |
| Update(//<absolute home>/.claude/**/*.md) | No |
| Update(~/.claude/rules/test-rule.md) (literal path) | No |
| Update(//<absolute home>/.claude/rules/test-rule.md) | No |
Notes
- Auto-memory edits (under
~/.claude/projects/<encoded-slug>/memory/*.md)
are silent — they appear to have implicit allowlisting independent of the
~/.claude/** allow rules.
- Per the docs (https://code.claude.com/docs/en/permissions.md),
~/path
is documented as supported ("Path from home directory") and ** is
documented as recursive across directories.
- The runtime prompt label uses
Update(~/...)form, but/permissions
lists rules with whatever scope name (Edit or Update) was put in
settings.json. Update(...) rules with the same path patterns also
fail to match.
- Whether project-level
.claude/settings.local.json(e.g. with
defaultMode: "acceptEdits" and project-relative Edit(.claude/**))
interacts with user-global lookup is untested.
Environment
- Claude Code: 2.1.133
- OS: Linux x86_64
Impact
Users who store global rules, scripts, or skills under ~/.claude/ (a
documented and encouraged location) cannot allowlist edits to those files
without a permission prompt every time, despite writing rules that match
both the documented syntax and the gitignore-style spec.
Showing cached comments. Read the full discussion on GitHub ↗
10 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Quick follow-up on the "is untested" line in the Notes section: ran the reproducer in a directory with no project-level
.claude/settings.local.jsondiscoverable at all. The prompt fires identically (Update(~/.claude/rules/test-rule.md)). Project-levelsettings.local.json(presence, contents, or absence) is not the cause — the bug reproduces purely on user-global allow-rule handling.Reviewed the three duplicates the bot flagged:
**allow rules not matching paths outside cwd). That issue's reproducer usesRead(/mnt/c/MCP/**)on WSL, ours usesEdit(~/.claude/**/*.md)on native Linux. Different specific paths, almost certainly the same underlying bug. Worth flagging that the failure mode isn't WSL- or Windows-mount-specific — it reproduces on a documented standard location (~/.claude/) on plain Linux.~/.claude/don't fire — which would be consistent with their premise that the expansion isn't actually happening at match time. May share a root cause.Not closing as duplicate of any of the three: #40076 is the closest but ours adds a distinct reproducer (documented
~/.claude/location, native Linux,Edit(...)andUpdate(...)scope-name forms both tested) that may help maintainers narrow root cause.Same loaded-but-unmatched symptom on different rule scopes (Claude Code 2.1.142, macOS).
Two examples from a single session, cwd is the project root (
~/<project>/), which contains a nested sub-project (~/<project>/<sub-project>/) with its own.claude/.1.
Skill(...)rule —Skill(<skill-name>)is present in both~/<project>/.claude/settings.local.jsonand the nested~/<project>/<sub-project>/.claude/settings.local.json./permissions→ search "skill" confirmsSkill(<skill-name>)is in the loaded allow list. Invoking the skill still fires a permission prompt.2.
mcp__...rule —mcp__<internal_google_workspace_mcp>__drive_get_file_by_urlis allowed in three settings files (user-global, shared project, nested project-local)./permissions→ search "drive_get_file" confirms bothmcp__<internal_google_workspace_mcp>__drive_get_fileandmcp__<internal_google_workspace_mcp>__drive_get_file_by_urlare in the loaded allow list. Calling the tool still fires a permission prompt offering "always allow."Ruled out on my side:
denyoraskentries; nodefaultMode/permissionModeoverride./Library/Application Support/ClaudeCode/managed-settings.json) only denies destructiverm/git push --force— doesn't touchSkill(...)ormcp__...scopes.Suggests the matcher regression isn't path-specific to
~/.claude/— it also affectsSkill(...)andmcp__...scope names.Symptom started ~2 days before reporting, which aligns roughly with the 2.1.142 release window for this account.
Another symptom on this thread: Read rules fail to match under subagent dispatch.
A subagent with
Readin its tool whitelist, dispatched against a path matched by a user-levelRead(~/.claude/**)allow rule, reports it could not read the file and falls back to whatever context was already loaded.State verified:
Read(~/.claude/**)is in the user-globalallowarray./permissionsshows it loaded.Read(...)deny entries in~/.claude/settings.json.Read.So the allow rule should match. Empirically it doesn't. Surfaces as "denied" rather than "prompted" because a subagent has no UI to accept a permission prompt — an unmatched allow rule with no fallback escape becomes a hard fail in the subagent's reasoning.
Adds Read in subagent dispatch to the bug's surface (alongside
Edit/Updatefor paths andSkill(...)/mcp__...for scope names per the 2026-05-15 comment). The 2026-05-08 verification deliberately left Read untested with the note that "reads have historically not prompted" — true for interactive sessions where the read-only tool tier auto-approves, but subagents go through the permission matcher on Read calls and an unmatched allow rule has no escape there.Adding a Linux-side data point that extends the surface further: project-relative
/pathrules in user-level~/.claude/settings.jsonalso load but don't fire at runtime. Bug persists at 2.1.143 (one patch after the 2.1.142 macOS report above).Environment
Setup
~/.claude/settings.json:(
/covered-path/**is project-root-relative per the docs' single-slash convention.)<project>/.claude/settings.local.json:Two probe files in the project working tree:
<project>/uncovered-path/probe.txt— no allow rule covers it.<project>/covered-path/probe.txt— covered by the user-level rule above.Test 1 — fresh subagent (general-purpose, model: sonnet)
"Permission to use Edit has been denied.").Test 2 — parent session
defaultMode: defaultis genuinely active at runtime).Interpretation
The rule appears in
/permissionsas loaded; it doesn't fire at match time for an Edit against a path it should cover. Same shape as the original~/.claude/**tilde-prefix failures and the 2026-05-15Skill(...)/mcp__*scope-name failures — now also reproducible with project-relative/pathrules in user-level settings, on Linux, at 2.1.143, for both fresh subagents and the parent session.Consistent with the matcher regression sitting at a layer shared across multiple rule scope types rather than being specific to any one path-syntax or scope-name family.
Side note on test setup. Mid-session changes to
defaultModein projectsettings.local.jsondid not take effect in the live session — a Claude Code restart was needed before the test became discriminating. May or may not be relevant to the matcher bug; flagging in case it's a clue about how settings are read.Same root cause as #36884: the
XIqrule predicate in claude.exe's permission matcher hard-rejects any rule withruleValue.ruleContent !== undefined, so all path-globbedEdit(...)/Read(...)/Write(...)and (per your May 17 comment)Skill(...)andmcp__...(...)rules fail to match for tools that don't have their own content matcher.Full disassembly + reproducer matrix in that comment. Treating these two as duplicates is probably the right move.
Still open and not confirmed resolved — commenting to prevent stale auto-closure. The root-cause analysis (permission-matcher disassembly) is on #36884, which this appears to duplicate; flagging that #57132 carries the accurate, surface-agnostic title, whereas #36884 reads as VS-Code-specific despite the bug reproducing cross-platform (macOS, Windows, Linux).
Following up: this appears fixed in current Claude Code. I re-ran the reproducer from my
earlier comment against the native CLI (
claude.exe2.1.207, Windows) in an isolated configsandbox, and path-globbed allow rules under
~/.claude/now match at runtime — anEdit(<glob>)allow that previously showed as loaded but never matched is now honored(allowed, no prompt), with a no-rule control still correctly blocked. Path-globbed deny rules
also take effect now.
I don't know exactly which release fixed it — it landed somewhere between 2.1.145 and 2.1.207
with no changelog note I could find — so if you're on an older build an upgrade is the fix.
For reference, the underlying matcher change appears to be an added content-matching path
rather than removal of the old reject filter (the original
ruleContent-reject line is stillin the 2.1.207 binary), which is consistent with the behavior now working while the old code
path remains.
Verification was native-CLI only; I didn't test the VS Code extension surface.
Confirmed on Linux, 2.1.207. The
//-absolute allow rules fire, but~/-tilde ones still don't. The tilde form is this issue's own reproducer, so #57132 as filed still reproduces. The residual looks like~not being expanded on the allow path (#55230), as opposed to the #36884ruleContentreject, which appears fixed for absolute globs.