Skip to content

fix(claude-config): audit-permission-grants P2 states an unsupported no-expansion rule and emits it on rule classes that do expand #2248

Description

@kyle-sexton

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:278is
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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions