Defect
audit-permission-grants check P2 tells authors that Bash rules match "with no ~/$HOME/env
expansion". The claim is unsupported by the official docs — not merely over-broad — and it is
contradicted by two documented substitutions, one of which the docs present as the zero-prompt
pattern. The same wrong sentence is emitted in the finding, and pinned by a passing test.
Evidence at HEAD
plugins/claude-config/skills/audit-permission-grants/reference/criteria.md:63-64:
**Why**: Bash rules match literally with no `~`/`$HOME`/env expansion, so the rule breaks on other
machines/usernames and leaks a username into version control. See convention anti-pattern 2.
Emitted at scripts/permission-rule-check.sh:139:
emit error P2 "$src" "hardcoded machine path in '$m' — Bash rules match literally (no ~/\$HOME/env expansion), so this breaks on other machines/usernames and leaks a username into source control. Use a machine-independent bare-name rule."
Pinned at scripts/permission-rule-check.test.sh:135:
assert_contains "P2 detail mentions literal matching" "$OUT" "no ~/\$HOME"
The shipped suite passes at HEAD (All 50 checks passed., run this pass), so correcting
criteria.md alone fails the suite — the test changes with it.
Docs check — verified this pass by raw fetch and grep, not by summarizing fetch
Both pages pulled with curl to a file and grepped (skills.md 87,211 bytes; permissions.md
61,351 bytes):
skills.md:333 — "Claude Code substitutes ${CLAUDE_SKILL_DIR} and ${CLAUDE_PROJECT_DIR} in two
places: the skill's markdown content, and Bash rules in the allowed-tools frontmatter. Using the
same variable in both places lets a skill run a bundled script without a permission prompt." The
canonical example at :339 is allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/render.sh *).
permissions.md:280 — `~/path` | Path from home directory | `Read(~/Documents/*.pdf)` | `/Users/alice/Documents/*.pdf`. Read/Edit rules use gitignore pattern syntax and do resolve
~/.
permissions.md:190 — "Claude Code also strips a leading assignment of certain known-safe
environment variables, so Bash(npm test *) matches NODE_ENV=test npm test. An allow rule won't
match past an assignment of any other variable. A deny or ask rule matches past any leading
assignment." Note the scoping: the stripping is not a blanket rule.
- The blanket claim itself is absent.
grep -in "no ~/\$HOME|match literally|literally with no|no expansion|does not expand" across both pages returns zero hits. The Bash section
(permissions.md:162-176) specifies wildcard glob matching over the command string and states no
no-expansion rule. Record the claim as unsupported, not as over-broad.
Reproduced this pass — the message is wrong on a whole rule class
The single message string at :139 is emitted for every rule class, not only Bash. Probed against
the shipped script:
| Rule |
Result |
Bash(/c/Users/kyle/x.sh:*) |
flagged P2 |
Read(/c/Users/kyle/notes.md) |
flagged P2, with the "Bash rules match literally" message |
Edit(/Users/alice/src/**) |
flagged P2, same message |
Read(~/Documents/notes.md) |
not flagged (correct) |
Bash(${CLAUDE_SKILL_DIR}/scripts/x.sh *) |
not flagged (correct) |
Bash(${CLAUDE_PROJECT_DIR}/scripts/lint.sh *) |
not flagged (correct) |
On a Read finding the message is wrong twice over: it names the wrong rule class, and it asserts a
no-expansion rule that is false for that class. The rewrite must be true for every class the check
fires on.
The corrected doctrine already exists in-repo
docs/conventions/permission-rule-hygiene/README.md:106-134 (anti-pattern 2) carries it, and is more
precise than the skill: it separates Bash literal-glob matching from the gitignore anchors used by the
file tools, names both substituted tokens, records that ${CLAUDE_PROJECT_DIR} substitution requires
Claude Code v2.1.196 or later, and records that ${CLAUDE_PLUGIN_ROOT} is not substituted so a
rule written with it is inert. The fix syncs criteria.md down from the convention; no convention edit
is needed.
Grading
Adjacent, deliberately not fixed here
criteria.md:60-61 claims //… forms are not flagged. Probing shows
Read(//Users/alice/secrets/**) — the docs' own literal example at permissions.md:278 — is
flagged. That is ledger row A2 (MED, implement_now: false), a different hunk; reproduced here as
corroborating evidence for it, not fixed by this change.
Defect
audit-permission-grantscheck P2 tells authors that Bash rules match "with no~/$HOME/envexpansion". The claim is unsupported by the official docs — not merely over-broad — and it is
contradicted by two documented substitutions, one of which the docs present as the zero-prompt
pattern. The same wrong sentence is emitted in the finding, and pinned by a passing test.
Evidence at HEAD
plugins/claude-config/skills/audit-permission-grants/reference/criteria.md:63-64:Emitted at
scripts/permission-rule-check.sh:139:Pinned at
scripts/permission-rule-check.test.sh:135:The shipped suite passes at HEAD (
All 50 checks passed., run this pass), so correctingcriteria.mdalone fails the suite — the test changes with it.Docs check — verified this pass by raw fetch and grep, not by summarizing fetch
Both pages pulled with
curlto a file and grepped (skills.md87,211 bytes;permissions.md61,351 bytes):
skills.md:333— "Claude Code substitutes${CLAUDE_SKILL_DIR}and${CLAUDE_PROJECT_DIR}in twoplaces: the skill's markdown content, and Bash rules in the
allowed-toolsfrontmatter. Using thesame variable in both places lets a skill run a bundled script without a permission prompt." The
canonical example at
:339isallowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/render.sh *).permissions.md:280—`~/path` | Path from home directory | `Read(~/Documents/*.pdf)` | `/Users/alice/Documents/*.pdf`. Read/Edit rules use gitignore pattern syntax and do resolve~/.permissions.md:190— "Claude Code also strips a leading assignment of certain known-safeenvironment variables, so
Bash(npm test *)matchesNODE_ENV=test npm test. An allow rule won'tmatch past an assignment of any other variable. A deny or ask rule matches past any leading
assignment." Note the scoping: the stripping is not a blanket rule.
grep -in "no ~/\$HOME|match literally|literally with no|no expansion|does not expand"across both pages returns zero hits. The Bash section(
permissions.md:162-176) specifies wildcard glob matching over the command string and states nono-expansion rule. Record the claim as unsupported, not as over-broad.
Reproduced this pass — the message is wrong on a whole rule class
The single message string at
:139is emitted for every rule class, not only Bash. Probed againstthe shipped script:
Bash(/c/Users/kyle/x.sh:*)Read(/c/Users/kyle/notes.md)Edit(/Users/alice/src/**)Read(~/Documents/notes.md)Bash(${CLAUDE_SKILL_DIR}/scripts/x.sh *)Bash(${CLAUDE_PROJECT_DIR}/scripts/lint.sh *)On a
Readfinding the message is wrong twice over: it names the wrong rule class, and it asserts ano-expansion rule that is false for that class. The rewrite must be true for every class the check
fires on.
The corrected doctrine already exists in-repo
docs/conventions/permission-rule-hygiene/README.md:106-134(anti-pattern 2) carries it, and is moreprecise than the skill: it separates Bash literal-glob matching from the gitignore anchors used by the
file tools, names both substituted tokens, records that
${CLAUDE_PROJECT_DIR}substitution requiresClaude Code v2.1.196 or later, and records that
${CLAUDE_PLUGIN_ROOT}is not substituted so arule written with it is inert. The fix syncs
criteria.mddown from the convention; no convention editis needed.
Grading
docs present as the zero-prompt pattern.
criteria.mdP1 Recommend line forprescribing an unavailable bare-name pattern. Different line, different check.
20260811-021645-adjacent permission-grants audit (batch-4 ledgerI10,row
A1).Adjacent, deliberately not fixed here
criteria.md:60-61claims//…forms are not flagged. Probing showsRead(//Users/alice/secrets/**)— the docs' own literal example atpermissions.md:278— isflagged. That is ledger row
A2(MED,implement_now: false), a different hunk; reproduced here ascorroborating evidence for it, not fixed by this change.