Three residual defects in claude-config:audit share one shape: the skill asserts more coverage or
more protection than it has. It presents a hook inventory that structurally cannot be complete — and
since #2206 the new baseline narrowing depends on exactly the inventory it cannot take. It ships an
error-rated Bash deny table drawn entirely from the pattern class upstream calls fragile, with no
caveat and one pattern that does not match the command it names. And the verbatim-fetch guard that
exists to stop a summarizing fetch reporting present rows as absent is attached to one checklist row,
not to the two Phase 3 fetches — one of which is the very page that false negative occurred on.
Rows
Evidence
A4 — missing reach, and #2206 made it load-bearing
Category D declares plugin hooks in scope. reference/audit-checklist.md:98 names the placeholders it
checks "in either spelling — braced (${CLAUDE_PROJECT_DIR}, ${CLAUDE_PLUGIN_ROOT},
${CLAUDE_PLUGIN_DATA}) or bare-dollar", and ${CLAUDE_PLUGIN_ROOT} / ${CLAUDE_PLUGIN_DATA} only
ever appear in a plugin-provided hook.
The enumeration never happens. SKILL.md:51-57 (the Config Files table Phase 1 loads) reads
.claude/settings.json, .claude/settings.local.json, .mcp.json, ~/.claude/settings.json, and the
managed-settings layer — and nothing else. SKILL.md:57 and context/validation-categories.md:75-125
(Category E) enumerate plugins by name only — marketplace membership plus drift — and never open a
component file.
What changed at HEAD, and why this row is sharper now than when it was written. #2206 added a third
baseline narrowing: an absent deny pattern whose family is blocked by a live PreToolUse hook drops
to info. Three surfaces now state, correctly, that the audit cannot take the inventory that narrowing
needs:
context/procedures.md:68-69 →
plugin's `hooks/hooks.json`, so this is a question to answer, not a lookup.
reference/required-permissions.md:184-186 →
**Fail open where no hook inventory was taken.** The audit has no enumeration path over a plugin's
`hooks/hooks.json` (it reads settings-declared hooks only), so on most runs you will not know what is
installed.
So the gap is now disclosed in three places and closed in none. grep -l "hooks.json" across the
skill returns context/procedures.md, evals/evals.json, and reference/required-permissions.md —
every hit is prose saying the audit cannot read it. The result is an off-ramp that, on most runs,
resolves to "state the finding conditionally" rather than to an answer, and a Category D hook inventory
that reads as complete while being settings-only by construction.
Do not carry the withdrawn remedy. The packet's addendum (which governs) withdrew the row
proposing "state Category D's settings-only hook scope explicitly". Narrowing a scope the checklist
already claims is the wrong direction; the correct defect is missing reach, and the remedy is an
enumeration path (a check-hook-coverage.sh-shaped step, or a Phase 1 read of enabled plugins'
hooks/hooks.json).
A2 — the baseline recommends the pattern class upstream calls fragile
reference/required-permissions.md:111-124 at HEAD is a table of eight argument-constraining globs
with no fragility caveat:
## destructive-bash-deny (Bash deny)
Bash deny patterns for destructive git operations — the universal baseline.
| Pattern | Blocks |
| --- | --- |
| `Bash(git push --force *)` | Force push with args |
| `Bash(git push --force)` | Force push without args |
| `Bash(git push -f *)` | Short flag force push with args |
| `Bash(git push -f)` | Short flag force push without args |
| `Bash(git reset --hard *)` | Hard reset with args |
| `Bash(git reset --hard)` | Hard reset without args |
| `Bash(git clean -f *)` | Force clean |
| `Bash(git clean -fd *)` | Force clean with directories |
Rated error when absent (reference/audit-checklist.md:29). The ranking that sends the reader away
from the alternative, same file :87-91:
**A `PreToolUse` hook on `Bash|PowerShell` is a speed bump, not a boundary.** It can inspect the
command string and deny the call, and a hook exiting 2 blocks a call an *allow* rule would otherwise
have permitted. A decision it returns cannot loosen a deny — see "Interaction with hook-based gates"
below for the precise ordering. But it inspects that same command string, so it inherits the evasion surface of a
Bash deny glob. Rank it below the sandbox and never describe it as protection.
The same file already quotes the warning — one section earlier, and against a different table.
required-permissions.md:57-60, inside sensitive-file-deny:
**Do not try to close the gap with `Bash(...)` deny globs.** The permissions page warns that "Bash
permission patterns that try to constrain command arguments are fragile", and the set of programs
that can open a file is unbounded. Enumerating readers relocates the false confidence instead of
removing it.
So the fragility caveat is present at :57-60 and absent at :111-124, where the file then ships
eight patterns of exactly that class as an error-rated floor. The inconsistency is internal, not only
against upstream.
https://code.claude.com/docs/en/permissions.md (raw curl, fetched 2026-08-11), line 218 warning
box: "Bash permission patterns that try to constrain command arguments are fragile."
The leak claim is fully confirmed at lines 165-174: "Bash(npm run test *) matches Bash commands
starting with npm run test" and "When * appears at the end with a space before it … it enforces a
word boundary." Matching is prefix-anchored, so git push origin main --force does not match
Bash(git push --force *).
Two precision notes the fix must keep. (1) The doc's stated remedy — "use PreToolUse hooks" — is
scoped on that page to URL filtering, while its fragility claim is general to argument-constraining
patterns. So "upstream ranks the hook above the glob for destructive commands" over-reaches; the
fragility half is what is supported. (2) The "speed bump" ranking is correct for the threat model it
was written under (secret exfiltration, "Scope of a Read deny"). The defect is that it is applied
unscoped, so it also governs the destructive-git model. Scope the ranking, do not delete it, and add
the fragility caveat plus the prefix-anchoring hole to the Bash deny table. #1598 (closed) is the exact
precedent: it added scope language to the Read deny table in this same file.
MN1 — the fetch-method guard is attached to one row
The guard exists, at exactly one place — reference/audit-checklist.md:116 (Category F, env vars):
**Read it verbatim, not through a summarizer** — the page is long (315 variable rows on 2026-08-10)
and a summarizing fetch truncates it, then reports the rows past the cutoff as absent
landed by #2185. But Phase 3.1 (SKILL.md:137-143, "Fetch code.claude.com/docs/en/settings and
compare") and Phase 3.4 (SKILL.md:161-167, "Fetch code.claude.com/docs/en/permissions and verify")
carry no verbatim-fetch instruction — and settings is the very page the originating false
negative occurred on (three settings keys reported NOT FOUND on a 290 KB page that raw curl + grep
found at lines 323-325). One sentence in a Phase 3 preamble generalizes the guard the repo already
merged for one row.
Adjacent, deliberately not duplicated
Provenance
Severity: MED (cluster highest; MN1 is LOW) · Provenance: AUDITOR_VERIFIED
Origin: handoff-inbox item 20260810-225905-claude-config-audit-category-b-hook-blindness
Ledger: .work/handoff-inbox-batch-4/ledgers/I2-claude-config-category-b.md § A4, A2, MN1
Verified against repo HEAD 9b34a82a. Line anchors re-derived at that commit; the ledger's were taken at dacb8661, before #2206 moved required-permissions.md and audit-checklist.md.
Three residual defects in
claude-config:auditshare one shape: the skill asserts more coverage ormore protection than it has. It presents a hook inventory that structurally cannot be complete — and
since #2206 the new baseline narrowing depends on exactly the inventory it cannot take. It ships an
error-rated Bash deny table drawn entirely from the pattern class upstream calls fragile, with nocaveat and one pattern that does not match the command it names. And the verbatim-fetch guard that
exists to stop a summarizing fetch reporting present rows as absent is attached to one checklist row,
not to the two Phase 3 fetches — one of which is the very page that false negative occurred on.
Rows
plugins/claude-config/skills/audit/SKILL.md:51-57,context/validation-categories.md:75-125— nothing in Phase 1, Category D, or Category E ever reads a plugin'shooks/hooks.json, though Category D writes rules for${CLAUDE_PLUGIN_ROOT}hooks and Category B's new narrowing 3 turns on whether such a hook is live.reference/required-permissions.md:111-124and:87-91— thedestructive-bash-denytable is eight argument-constraining globs ratederrorwhen absent, with no fragility caveat;Bash(git push --force *)does not matchgit push origin main --force; and the "speed bump" ranking of thePreToolUsehook is stated unscoped.reference/audit-checklist.md:116vsSKILL.md:137-143and:161-167— the read-it-verbatim guard exists on exactly one row (Category F env vars) while Phase 3.1 and Phase 3.4 fetchsettingsandpermissionswith no such instruction.Evidence
A4 — missing reach, and #2206 made it load-bearing
Category D declares plugin hooks in scope.
reference/audit-checklist.md:98names the placeholders itchecks "in either spelling — braced (
${CLAUDE_PROJECT_DIR},${CLAUDE_PLUGIN_ROOT},${CLAUDE_PLUGIN_DATA}) or bare-dollar", and${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PLUGIN_DATA}onlyever appear in a plugin-provided hook.
The enumeration never happens.
SKILL.md:51-57(the Config Files table Phase 1 loads) reads.claude/settings.json,.claude/settings.local.json,.mcp.json,~/.claude/settings.json, and themanaged-settings layer — and nothing else.
SKILL.md:57andcontext/validation-categories.md:75-125(Category E) enumerate plugins by name only — marketplace membership plus drift — and never open a
component file.
What changed at HEAD, and why this row is sharper now than when it was written. #2206 added a third
baseline narrowing: an absent deny pattern whose family is blocked by a live
PreToolUsehook dropsto
info. Three surfaces now state, correctly, that the audit cannot take the inventory that narrowingneeds:
context/procedures.md:68-69→reference/required-permissions.md:184-186→So the gap is now disclosed in three places and closed in none.
grep -l "hooks.json"across theskill returns
context/procedures.md,evals/evals.json, andreference/required-permissions.md—every hit is prose saying the audit cannot read it. The result is an off-ramp that, on most runs,
resolves to "state the finding conditionally" rather than to an answer, and a Category D hook inventory
that reads as complete while being settings-only by construction.
Do not carry the withdrawn remedy. The packet's addendum (which governs) withdrew the row
proposing "state Category D's settings-only hook scope explicitly". Narrowing a scope the checklist
already claims is the wrong direction; the correct defect is missing reach, and the remedy is an
enumeration path (a
check-hook-coverage.sh-shaped step, or a Phase 1 read of enabled plugins'hooks/hooks.json).A2 — the baseline recommends the pattern class upstream calls fragile
reference/required-permissions.md:111-124at HEAD is a table of eight argument-constraining globswith no fragility caveat:
Rated
errorwhen absent (reference/audit-checklist.md:29). The ranking that sends the reader awayfrom the alternative, same file
:87-91:The same file already quotes the warning — one section earlier, and against a different table.
required-permissions.md:57-60, insidesensitive-file-deny:So the fragility caveat is present at
:57-60and absent at:111-124, where the file then shipseight patterns of exactly that class as an
error-rated floor. The inconsistency is internal, not onlyagainst upstream.
https://code.claude.com/docs/en/permissions.md (raw
curl, fetched 2026-08-11), line 218 warningbox: "Bash permission patterns that try to constrain command arguments are fragile."
The leak claim is fully confirmed at lines 165-174: "
Bash(npm run test *)matches Bash commandsstarting with
npm run test" and "When*appears at the end with a space before it … it enforces aword boundary." Matching is prefix-anchored, so
git push origin main --forcedoes not matchBash(git push --force *).Two precision notes the fix must keep. (1) The doc's stated remedy — "use PreToolUse hooks" — is
scoped on that page to URL filtering, while its fragility claim is general to argument-constraining
patterns. So "upstream ranks the hook above the glob for destructive commands" over-reaches; the
fragility half is what is supported. (2) The "speed bump" ranking is correct for the threat model it
was written under (secret exfiltration, "Scope of a Read deny"). The defect is that it is applied
unscoped, so it also governs the destructive-git model. Scope the ranking, do not delete it, and add
the fragility caveat plus the prefix-anchoring hole to the Bash deny table. #1598 (closed) is the exact
precedent: it added scope language to the Read deny table in this same file.
MN1 — the fetch-method guard is attached to one row
The guard exists, at exactly one place —
reference/audit-checklist.md:116(Category F, env vars):landed by #2185. But Phase 3.1 (
SKILL.md:137-143, "Fetch code.claude.com/docs/en/settings andcompare") and Phase 3.4 (
SKILL.md:161-167, "Fetch code.claude.com/docs/en/permissions and verify")carry no verbatim-fetch instruction — and
settingsis the very page the originating falsenegative occurred on (three settings keys reported NOT FOUND on a 290 KB page that raw
curl+grepfound at lines 323-325). One sentence in a Phase 3 preamble generalizes the guard the repo already
merged for one row.
Adjacent, deliberately not duplicated
Provenance
Severity: MED (cluster highest; MN1 is LOW) · Provenance: AUDITOR_VERIFIED
Origin: handoff-inbox item
20260810-225905-claude-config-audit-category-b-hook-blindnessLedger:
.work/handoff-inbox-batch-4/ledgers/I2-claude-config-category-b.md§ A4, A2, MN1Verified against repo HEAD
9b34a82a. Line anchors re-derived at that commit; the ledger's were taken atdacb8661, before #2206 movedrequired-permissions.mdandaudit-checklist.md.