Skip to content

fix(claude-config): audit-permission-grants' criteria.md is a stale pinned copy, and its remediation advice is wrong in four places downstream #2284

Description

@kyle-sexton

audit-permission-grants keeps its doctrine in reference/criteria.md, a pinned copy of the
permission-rule-hygiene convention. The copy still carries Version: 1.0.0 / Last updated: 2026-07-14
while the convention has moved five times since, and there is no version handshake in either direction —
so the drift is silent and is the cause of the rest of this cluster rather than a peer of it. #2259
synced P2's rationale down without bumping either stamp, which is the drift reproducing in real time.

Downstream of that: the P1 finding message scolds a shape Anthropic's own docs ship and omits the
turn-scope limiter that changes what the grant even means; the package-manager extension reaches past
the documented category with no honesty marker, while the fixed-path-interpreter case two sections away
does carry one; the operative remediation lands in a file the detector cannot read; the plugin bin/
capability that would make the prescribed pattern achievable is never mentioned; the route-out has no
branch for the sibling being absent; and the description over-triggers into that sibling's territory.

Rows

  • A14reference/criteria.md:3-4 and :9Version: 1.0.0, Last updated: 2026-07-14, citing the convention at an unpinned main URL. The convention has moved five times since; no handshake exists in either direction. fix(claude-config): drop P2's unsupported no-expansion claim, refuse an unresolvable scan root, and key the postures report per project #2259 synced content down without touching either stamp.
  • A13scripts/permission-rule-check.sh:172 — the P1 message tells the author their grant "is an interpreter/runner-led grant, not the portable bare-name pattern" (a shape the docs themselves ship) and never mentions that the grant is turn-scoped.
  • A18reference/criteria.md:38-40 vs :52-54 — the bare-package-manager-wildcard extension reaches past the documented "package-manager run commands" category with no marker, while the fixed-path-interpreter case in the same file carries one.
  • A6SKILL.md:43-44, reference/criteria.md:109-111, scripts/permission-rule-check.sh:293 vs :280-281 — every P3 remediation says the operative rule must go in ~/.claude/settings.json; no code path in the script ever reads ~/.claude/settings.json or $HOME.
  • A10plugins/claude-config/bin does not exist; grep -rn 'bin/' across the skill returns only shebangs and test fixtures — the plugin bin/ capability is never mentioned, in the skill whose whole remedy is a bare name on PATH.
  • A17SKILL.md:25-31 and reference/criteria.md:83-89 — both route out to sibling audit skills unconditionally, with no "if that skill is not installed, do X" branch.
  • A9SKILL.md:2 with disable-model-invocation: false at :5 — the description's trigger list opens with 'check permission rules' and includes 'audit allow rules', both generic enough to match the sibling claude-config:audit; the negative boundary is body-only and invisible at discovery time.
  • A7a (residue) — reference/criteria.md:56-57, SKILL.md, scripts/permission-rule-check.test.sh${CLAUDE_SKILL_DIR} now appears in P2 and in the P2 finding message (fix(claude-config): drop P2's unsupported no-expansion claim, refuse an unresolvable scan root, and key the postures report per project #2259), but P1's Recommend still prescribes only bare-name-on-PATH, and the variable appears nowhere in SKILL.md or the test suite. P1 is the check every fleet grant trips.

Evidence

A14 — a pinned copy against an unpinned upstream

reference/criteria.md:3-4 at HEAD:

Version: 1.0.0
Last updated: 2026-07-14

citing the convention at an unpinned URL (criteria.md:9). The convention has moved five times
since that date: 4c7c21a6 (#1277), 6c1f9e8f (#1399), e3bfa56a (#1861), 29f0759f (#2060),
9d9fe485 (#2180). Concrete divergences, not hypothetical:

Corroboration that the drift bites: plugins/repo-fleet-hygiene/skills/audit/SKILL.md:6 ships
Bash(bash ${CLAUDE_SKILL_DIR}/scripts/audit-fleet.sh *) — the fleet was already using the variable
criteria.md did not name and whose P1 message tells the author to abandon.

Fix shape, and it is smaller than the originating item assumed. The item's remediation said land the
doctrine in the convention first. At HEAD the convention is already ahead on every point, so this
reduces to: sync criteria.md down, bump Version / Last updated, and add a handshake line naming
the convention commit it was synced from. No convention edit is required.

A13 — the P1 message is wrong in two ways

scripts/permission-rule-check.sh:172 at HEAD:

    [[ -n "$m" ]] && emit warning P1 "$src" "'$m' is an interpreter/runner-led grant, not the portable bare-name pattern; Claude Code drops the broad forms of this shape (blanket, package-manager runners, and wildcarded/globbed-target interpreters) on entering auto mode. Expose the guarded script as a bare PATH command and allow that, e.g. Bash(babysit_merge.sh:*)."
  1. The style scolding is contradicted by Anthropic's own page. https://code.claude.com/docs/en/skills, fetched 2026-08-11, ships allowed-tools: Bash(python3 *) in its codebase-visualizer tutorial.
  2. The turn-scope limiter is missing, and it is material. Same page: "The allowed-tools field grants permission for the listed tools during the turn that invokes the skill … The grant clears when you send your next message, even though the skill content stays in context; invoking the skill again re-applies it for that turn." grep -rni 'next message|turn-scoped|turn that invokes' across the whole skill → zero hits.
  3. The security framing the message should carry is the docs' own: "Review project skills before trusting a repository, since a skill can grant itself broad tool access."

This is the message the authors of every fleet grant read when they go to fix one, which is why it
outranks the other text rows.

A18 — an undisclosed extension, next to a disclosed one

reference/criteria.md:38-40 flags bare package-manager wildcards with the rationale inline and no
marker
that this reaches past the documented category:

package-manager grant — a runner subcommand (`Bash(npx *)`, `Bash(uvx *)`, `Bash(pipx run *)`,
`Bash(pnpm dlx *)`, …) or a bare package-manager wildcard (`Bash(npm:*)`, `Bash(npm *)`,
`Bash(pnpm:*)`, `Bash(yarn:*)`), which grants arbitrary execution via `npm exec` / lifecycle scripts —

Contrast the fixed-path-interpreter case at criteria.md:52-54, which does carry one: "…is flagged
as the same authoring anti-pattern even where the doc's dropped-category wording does not clearly reach
it". https://code.claude.com/docs/en/permission-modes, fetched 2026-08-11: "…Package-manager run
commands … Narrow rules like Bash(npm test) carry over." The check reaches bare package-manager
wildcards, which is broader. The extension is defensible; the silence about it is the defect, and the
asymmetry with the marker two sections away is what makes it a defect rather than a style choice.

Drop the item's stated justification for this row. It argued that the context7/setup finding was
"the concrete demonstration that A18 is material." That argument does not survive — that flag was a
regex over-reach, filed separately in this batch. A18 stands on the internal asymmetry alone.

A6 — the skill cannot audit the scope it prescribes

The prescription: SKILL.md:43-44 ("the bare-name rule must land in user-global ~/.claude/settings.json,
which a skill or plugin cannot write"), reference/criteria.md:109-111 ("the operative rule must be
added by the operator to ~/.claude/settings.json"), and scripts/permission-rule-check.sh:293 (the
P3 finding message, same sentence).

The scan surface: scripts/permission-rule-check.sh:280-281 reads only "$ROOT/.claude/settings.json"
and "$ROOT/.claude/settings.local.json". No code path anywhere in the script reads
~/.claude/settings.json or $HOME
— and after #2249 the root can no longer fall through to a home
directory even accidentally. A genuine closed loop, not an oversight in one file.

The P3 premise itself is confirmed: https://code.claude.com/docs/en/plugins-reference, fetched
2026-08-11, components table — "Settings | settings.json | Default configuration applied when the
plugin is enabled. Only the agent and subagentStatusLine keys are currently supported". Note the
word "currently", which both of the skill's restatements (criteria.md:109-110,
scripts/permission-rule-check.sh:293) drop — a fidelity nit to fold in, and a reason to re-check that
quote on a harness bump.

A10 — bin/ is never mentioned, and the naive fix would re-open #1398

plugins/claude-config/bin does not exist. grep -rn 'bin/' across the skill returns only shebangs
and .venv/bin/python test fixtures. The capability is real —
https://code.claude.com/docs/en/plugins-reference, fetched 2026-08-11: "Executables | bin/ |
Executables added to the Bash tool's PATH. Files here are invokable as bare commands in any Bash tool
call while the plugin is enabled."

The originating framing is over-optimistic and must not be carried through. It presents bin/ as
"what makes the skill's own bare-name-on-PATH advice achievable." This repo's own convention says the
opposite at docs/conventions/permission-rule-hygiene/README.md:177-179: "Known gap — step 1's plugin
bin/ delivery is unreliable, not absent … a helper whose only permission story is bin/-on-PATH has no
allow rule it can depend on." A fix that documents bin/ as the answer without that caveat re-opens
#1398. Mention bin/, and mention the Known gap in the same breath.

A17 — route-out with no absent-seam branch

SKILL.md:25-31 and reference/criteria.md:83-89 both route out to the sibling audit skills
unconditionally, with no "if that skill is not installed, do X" branch. Both targets exist in this
repository (plugins/claude-config/skills/audit/, plugins/claude-memory/skills/audit/), so the
coherence half of the claim holds and the fallback half is simply absent. This is a conditional gap —
it bites only where the sibling plugin is not installed — and it couples to A6: the second half of the
row is that neither sibling covers the user-global scope either.

A9 — the description over-triggers into the sibling's territory

SKILL.md:2 — the description's trigger list opens with 'check permission rules' and includes
'audit allow rules', both generic enough to match the sibling claude-config:audit skill's remit
(baseline deny/ask presence, overly broad patterns, plugin drift). The negative boundary is body-only —
SKILL.md:25-31, "Scope boundary (route out)" — and invisible at discovery time.
disable-model-invocation: false at SKILL.md:5 means the model can select on the description alone.
(No harness doc governing description-based selection was fetched; this row stands on the two surfaces
above, not on a harness-behavior claim.)

A7a — the documented cheap fix reached P2 and not P1

When this was filed, ${CLAUDE_SKILL_DIR} appeared nowhere in plugins/claude-config/ — zero hits
across SKILL.md, criteria.md, the detector, and the tests — so the skill prescribed a remedy a tier
more expensive than the documented zero-operator-action pattern. #2259 fixed part of that: the variable
now appears at reference/criteria.md:82, :86, :99 and in the P2 finding message at
scripts/permission-rule-check.sh:175.

The residue is P1, which is the check that matters more here. reference/criteria.md:56-57:

**Recommend**: expose the helper as a bare command on PATH and allow the bare name narrowly. An
`Agent` rule has no bare-PATH analog — remove or re-scope it, or run outside auto mode.

Every interpreter-led fleet grant trips P1, not P2, so P1's Recommend is the line their authors read —
and it still names only the pattern #1398 says is unavailable on the measured platform. The variable
also still appears zero times in SKILL.md and zero times in
scripts/permission-rule-check.test.sh, so nothing pins that a
Bash(${CLAUDE_SKILL_DIR}/scripts/x.sh *) rule is not flagged.

https://code.claude.com/docs/en/skills, fetched 2026-08-11: "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," with the canonical example
allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/render.sh *); and "${CLAUDE_SKILL_DIR} — The
directory containing the skill's SKILL.md file. For plugin skills, this is the skill's subdirectory
within the plugin, not the plugin root." That second clause is load-bearing for anyone writing the P1
recommendation and holds today.

Adjacent, deliberately not duplicated

Provenance

Severity: MED (cluster highest; A9, A10, A17, A18 are LOW) · Provenance: AUDITOR_VERIFIED. Two rationale corrections are the validation pass's own and are stated inline: A10's bin/-is-the-answer framing is partially falsified by this repo's own Known gap, and A18's context7 justification does not survive.
Origin: handoff-inbox item 20260811-024628-claude-config-audit-permission-grants-defects-and-fleet-grant-hygiene
Ledger: .work/handoff-inbox-batch-4/ledgers/I10-permission-grants-fleet.md § A6, A7a, A9, A10, A13, A14, A17, A18
Verified against repo HEAD 9b34a82a. Line anchors re-derived at that commit — #2259 added 44 lines to criteria.md and 52 to the detector.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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