You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
audit-permission-grants reports a clean bill without ever reporting a denominator. "No fragile
permission grants found." prints identically whether it parsed forty allowed-tools blocks and found
them healthy or parsed none at all — and the skill's own body tells the operator to take that string at
face value. Around that one string sit four amplifiers: the only loadability filter is vendor/, so
non-loadable copies are scanned and loadable surfaces outside the root are not; the audited repo can
declare its own exemptions with no disclosure requirement, which makes a suppressed report
indistinguishable from a clean one; the four advertised scope filters are not implemented, so scope: settings still pays the full frontmatter find cost; and the only lever that scopes the scan is still
named as a test fixture.
#2249 closed the worst case — an unresolvable root now exits 2 instead of sweeping the user profile —
but a resolved root that contains nothing still prints the clean-bill string.
Rows
A5 — scripts/permission-rule-check.sh:303-305 and SKILL.md:81 — "clean" and "scanned nothing" are the same string; nothing counts files visited, allowed-tools blocks parsed, or allow rules read.
A8 — scripts/permission-rule-check.sh:258-262 — the only exclusion is ! -path '*/vendor/*'; the rationale that justifies it generalizes to node_modules/, worktrees, marketplace mirrors, and versioned cache copies, and is applied exactly once.
A15 — SKILL.md:85-89 — a consuming repo may declare additional fragile tokens or a documented exemption, with no disclosure requirement, no narrowing-only constraint, and no separate reporting slot.
A16 — SKILL.md:3, :35-40, :63-64 vs scripts/permission-rule-check.sh:55-69 — the four scope filters are advertised in the argument hint and the body, and the detector's entire argument surface is --count / --help.
scripts/permission-rule-check.sh:303-305 at HEAD, unchanged:
if [[ "${#findings[@]}" -eq 0 ]]; then
echo "No fragile permission grants found."
else
Nothing in the script counts files visited, allowed-tools blocks parsed, or allow rules read. SKILL.md:81 compounds it: "A clean scan ("No fragile permission grants found.") is a valid outcome —
report it as such."
(The originating observation — that the audited surface had zero allow rules and zero local allowed-tools, so the operator received a clean bill from a scan of nothing — is an environment fact
the validation pass did not re-measure. The code defect above is at HEAD.)
This is also the mechanism A15 exploits, and the reason A4/#2249's refusal branch had to be extended to --count: a count of 0 from a scan that never resolved a root reads exactly like a clean bill. The same
argument applies one level up — a count of 0 from a root with nothing in it reads the same as a count of
0 from a healthy root.
A8 — one exclusion, a rationale that generalizes
scripts/permission-rule-check.sh:258-262 — the frontmatter walk:
! -path '*/vendor/*' is the only exclusion. No exclusion for node_modules/, worktrees, marketplace
mirrors, or versioned plugin-cache copies, and no consultation of installed_plugins.json. The stated
rationale that would extend to all of them is at reference/criteria.md:18-20 — vendored copies are
"not loadable skills/agents/commands, so their allowed-tools never take effect and a finding on them
would be a false positive."
Scope this honestly. The originating measurement (448 raw findings → 109 after filtering to live
installs) was taken against the plugin cache, not this repository, and the validation pass did not
re-scan that tree. Inside this repo the noise sources it measured largely do not exist, so this row is
LOW at HEAD; it bites when the skill is pointed at a home directory — i.e. it is really an amplifier of
the fail-open scan #2249 just closed.
A15 — the audited repo can narrow its own audit
SKILL.md:85-89 at HEAD:
A consuming repo may declare, in its own `CLAUDE.md` / `.claude/rules/`, additional interpreter tokens
or path shapes it treats as fragile, or a documented exemption (e.g. a deliberately broad grant behind
a PreToolUse hook). Read those when present; this skill does not assume them.
No disclosure requirement, no narrowing-only constraint, no separate reporting slot. Combined with A5's
identical clean/empty string, a suppressed report is indistinguishable from a clean one. The escalation
is real because the audited repo authors the exemptions, so the input is attacker-controllable in the
threat model the docs themselves name — https://code.claude.com/docs/en/skills, fetched 2026-08-11:
"Review project skills before trusting a repository, since a skill can grant itself broad tool access."
Graded MED rather than higher because the surface is advisory report text, not an enforcement boundary.
The minimum fix is a disclosure slot: every exemption applied gets named in the report, and an exemption
may widen the finding set but never silence one without saying so.
and enumerated at SKILL.md:35-40. The implementation, SKILL.md:63-64: "If a scope filter was given,
run the full detector and present only the matching checks (P1/P2 map to …)". The detector's whole
argument surface is --count / --help (scripts/permission-rule-check.sh:55-69); grep -n 'scope\|--frontmatter\|--settings\|--plugins' finds no flag. So scope: settings still pays
the full frontmatter find cost, which is the cost that made the unbounded-root case expensive.
A11 — the one scoping lever is still named for tests
grep -rn 'PERMISSION_HYGIENE_FIXTURE_DIR' plugins/claude-config/ at HEAD returns hits in the script
header, the root-resolution branch at scripts/permission-rule-check.sh:71-72, the --help text, the
test harness — and, new since #2259, SKILL.md:59 and the --help body, both telling the operator to
"set $PERMISSION_HYGIENE_FIXTURE_DIR explicitly" when no root resolves.
So the undocumented half of this row is fixed and the misnamed half is not. The variable is now
the documented operator-facing remedy for an exit-2 refusal, while its name still says it is a test
fixture — which is exactly the friction the row was filed for. reference/criteria.md still does not
mention it. Either rename it with a back-compatible alias, or state in criteria.md that using it in
production is sanctioned.
Severity: MED (cluster highest; A8, A16 are LOW) · Provenance: AUDITOR_VERIFIED. Two legs are explicitly not re-measured by the validation pass and are marked inline: A8's 448→109 filtering figure (taken against the plugin cache, not this repo) and A5's supporting environment observation.
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 § A5, A8, A11, A15, A16
Verified against repo HEAD 9b34a82a. Line anchors re-derived at that commit — #2259 moved the detector's root resolution and added six lines to SKILL.md.
audit-permission-grantsreports a clean bill without ever reporting a denominator. "No fragilepermission grants found." prints identically whether it parsed forty
allowed-toolsblocks and foundthem healthy or parsed none at all — and the skill's own body tells the operator to take that string at
face value. Around that one string sit four amplifiers: the only loadability filter is
vendor/, sonon-loadable copies are scanned and loadable surfaces outside the root are not; the audited repo can
declare its own exemptions with no disclosure requirement, which makes a suppressed report
indistinguishable from a clean one; the four advertised scope filters are not implemented, so
scope: settingsstill pays the full frontmatterfindcost; and the only lever that scopes the scan is stillnamed as a test fixture.
#2249 closed the worst case — an unresolvable root now exits 2 instead of sweeping the user profile —
but a resolved root that contains nothing still prints the clean-bill string.
Rows
scripts/permission-rule-check.sh:303-305andSKILL.md:81— "clean" and "scanned nothing" are the same string; nothing counts files visited,allowed-toolsblocks parsed, or allow rules read.scripts/permission-rule-check.sh:258-262— the only exclusion is! -path '*/vendor/*'; the rationale that justifies it generalizes tonode_modules/, worktrees, marketplace mirrors, and versioned cache copies, and is applied exactly once.SKILL.md:85-89— a consuming repo may declare additional fragile tokens or a documented exemption, with no disclosure requirement, no narrowing-only constraint, and no separate reporting slot.SKILL.md:3,:35-40,:63-64vsscripts/permission-rule-check.sh:55-69— the four scope filters are advertised in the argument hint and the body, and the detector's entire argument surface is--count/--help.scripts/permission-rule-check.sh:71-72,:85-86—PERMISSION_HYGIENE_FIXTURE_DIRis the first branch of root resolution and therefore the only override; fix(claude-config): permission-rule-check falls through to $PWD and sweeps the user profile, exiting 0 #2249 documented it inSKILL.md:59and--help, but its name still tells an operator it is for tests.Evidence
A5 — no denominator
scripts/permission-rule-check.sh:303-305at HEAD, unchanged:Nothing in the script counts files visited,
allowed-toolsblocks parsed, or allow rules read.SKILL.md:81compounds it: "A clean scan ("No fragile permission grants found.") is a valid outcome —report it as such."
(The originating observation — that the audited surface had zero
allowrules and zero localallowed-tools, so the operator received a clean bill from a scan of nothing — is an environment factthe validation pass did not re-measure. The code defect above is at HEAD.)
This is also the mechanism A15 exploits, and the reason A4/#2249's refusal branch had to be extended to
--count: a count of 0 from a scan that never resolved a root reads exactly like a clean bill. The sameargument applies one level up — a count of 0 from a root with nothing in it reads the same as a count of
0 from a healthy root.
A8 — one exclusion, a rationale that generalizes
scripts/permission-rule-check.sh:258-262— the frontmatter walk:! -path '*/vendor/*'is the only exclusion. No exclusion fornode_modules/, worktrees, marketplacemirrors, or versioned plugin-cache copies, and no consultation of
installed_plugins.json. The statedrationale that would extend to all of them is at
reference/criteria.md:18-20— vendored copies are"not loadable skills/agents/commands, so their
allowed-toolsnever take effect and a finding on themwould be a false positive."
Scope this honestly. The originating measurement (448 raw findings → 109 after filtering to live
installs) was taken against the plugin cache, not this repository, and the validation pass did not
re-scan that tree. Inside this repo the noise sources it measured largely do not exist, so this row is
LOW at HEAD; it bites when the skill is pointed at a home directory — i.e. it is really an amplifier of
the fail-open scan #2249 just closed.
A15 — the audited repo can narrow its own audit
SKILL.md:85-89at HEAD:No disclosure requirement, no narrowing-only constraint, no separate reporting slot. Combined with A5's
identical clean/empty string, a suppressed report is indistinguishable from a clean one. The escalation
is real because the audited repo authors the exemptions, so the input is attacker-controllable in the
threat model the docs themselves name — https://code.claude.com/docs/en/skills, fetched 2026-08-11:
"Review project skills before trusting a repository, since a skill can grant itself broad tool access."
Graded MED rather than higher because the surface is advisory report text, not an enforcement boundary.
The minimum fix is a disclosure slot: every exemption applied gets named in the report, and an exemption
may widen the finding set but never silence one without saying so.
A16 — advertised, not implemented
Advertised at
SKILL.md:3:and enumerated at
SKILL.md:35-40. The implementation,SKILL.md:63-64: "If a scope filter was given,run the full detector and present only the matching checks (P1/P2 map to …)". The detector's whole
argument surface is
--count/--help(scripts/permission-rule-check.sh:55-69);grep -n 'scope\|--frontmatter\|--settings\|--plugins'finds no flag. Soscope: settingsstill paysthe full frontmatter
findcost, which is the cost that made the unbounded-root case expensive.A11 — the one scoping lever is still named for tests
grep -rn 'PERMISSION_HYGIENE_FIXTURE_DIR' plugins/claude-config/at HEAD returns hits in the scriptheader, the root-resolution branch at
scripts/permission-rule-check.sh:71-72, the--helptext, thetest harness — and, new since #2259,
SKILL.md:59and the--helpbody, both telling the operator to"set
$PERMISSION_HYGIENE_FIXTURE_DIRexplicitly" when no root resolves.So the undocumented half of this row is fixed and the misnamed half is not. The variable is now
the documented operator-facing remedy for an exit-2 refusal, while its name still says it is a test
fixture — which is exactly the friction the row was filed for.
reference/criteria.mdstill does notmention it. Either rename it with a back-compatible alias, or state in
criteria.mdthat using it inproduction is sanctioned.
Adjacent, deliberately not duplicated
$PWDfallback and added the exit-2 refusal, including for--count. This issue is the residue: a resolved root with nothing in it still reports clean.Provenance
Severity: MED (cluster highest; A8, A16 are LOW) · Provenance: AUDITOR_VERIFIED. Two legs are explicitly not re-measured by the validation pass and are marked inline: A8's 448→109 filtering figure (taken against the plugin cache, not this repo) and A5's supporting environment observation.
Origin: handoff-inbox item
20260811-024628-claude-config-audit-permission-grants-defects-and-fleet-grant-hygieneLedger:
.work/handoff-inbox-batch-4/ledgers/I10-permission-grants-fleet.md§ A5, A8, A11, A15, A16Verified against repo HEAD
9b34a82a. Line anchors re-derived at that commit — #2259 moved the detector's root resolution and added six lines toSKILL.md.