Skip to content

fix(claude-config): audit claims hook coverage it cannot enumerate, and a Bash deny baseline it never caveats #2275

Description

@kyle-sexton

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

  • A4plugins/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's hooks/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.
  • A2reference/required-permissions.md:111-124 and :87-91 — the destructive-bash-deny table is eight argument-constraining globs rated error when absent, with no fragility caveat; Bash(git push --force *) does not match git push origin main --force; and the "speed bump" ranking of the PreToolUse hook is stated unscoped.
  • MN1reference/audit-checklist.md:116 vs SKILL.md:137-143 and :161-167 — the read-it-verbatim guard exists on exactly one row (Category F env vars) while Phase 3.1 and Phase 3.4 fetch settings and permissions with no such instruction.

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.

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