hook-stocktake · git:20261031.cac9d1a · 2026-10-31 · sha256 508e2b5a057c7f4e
hook-stocktake git:20261031.cac9d1aB
Immutable. This exact content never changes and is served at /api/v1/blob/508e2b5a057c7f4e.
---
name: hook-stocktake
description: "Audit the harness hooks (hooks/*.sh, scripts/hooks/*) and their settings.json wiring, and give each a verdict (hook の棚卸し)."
origin: shimo4228
user-invocable: true
disable-model-invocation: true
---
# hook-stocktake — Hook Quality Audit
Evaluate every hook of the harness and assign each a verdict: `Keep / Improve / Update / Merge /
Dissolve / Retire`. A hook's cost is unlike a skill's or a rule's: it runs unattended on every
matching event, it can block, and it reads repo-controlled data (paths, commit metadata, tool
input). So the questions are wiring, evidence that it still earns its place, test coverage, and
its share of the harness threat surface (rule `security.md`).
## Phase 1 — Inventory and wiring (deterministic)
Build the inventory with throwaway `python3` / `jq` over the files, and state the counts up front:
- **Targets**: `hooks/*.sh` and `scripts/hooks/*.py`. `hooks/_*-common.sh` are shared libraries:
list which hooks source each one (`grep -l 'source.*_<name>' hooks/*.sh`)
- **Wiring**: from `~/.claude/settings.json` `hooks`, one row per (event, matcher, command).
Mark every target no command references (orphan) and every command that carries logic beyond
invoking a script (CLAUDE.md: hook logic lives in scripts). Commands whose script is missing
(dangling) come from `python3 ~/.claude/scripts/hooks/harness_lint.py`, which checks them.
`scripts/hooks/*.py` are called from hooks or from other scripts; resolve their callers with grep
- **Documentation**: `hooks/README.md` — the Active Hooks tables against the wiring rows, and its
Shared list against `hooks/_*-common.sh` (missing and phantom rows)
- **Tests**: the `tests/*.bats` files that exercise each hook (`grep -l <script name> tests/*.bats`)
- **Fire evidence**: only the logging hooks leave a count — `log-skill-usage.sh` and
`log-agent-usage.sh` append to `~/.claude/metrics/skill-usage.jsonl` / `agent-usage.jsonl`, which
count logged skill and agent events (`log-skill-usage.sh` is wired on two events), not every run
of a hook. Every other hook gets `—`, which is not zero. A `metrics/*.jsonl` that no hook or script
writes any more is an integrity finding
The wiring, dangling and README checks are integrity findings that feed Phase 2 as evidence.
## Phase 2 — Evaluation (inline, holistic)
Read every hook's header and body while seeing the whole set.
**Stage 1 — binary screen (every hook).** Answer Yes/No per hook; surface only the No answers:
- [ ] Wired, or sourced by a wired hook? (Phase 1)
- [ ] Logic lives in the script, not in `settings.json`? (Phase 1)
- [ ] A test exercises it? (blocking and advisory hooks both; a hook that blocks without a test is
an Improve candidate)
- [ ] Its own retirement condition has not fired? Some headers and README rows state one (for
example `bandit-precommit.sh` and `ruff-format-precommit.sh` retire once every Python repo has
its own `.claude/verify.sh`). Check the condition against the current state, unconditionally
- [ ] Not absorbed by the substrate? Can Claude Code now do this natively (permission rules,
sandbox, a built-in hook or check)? Verify against the current Claude Code docs (agent:
`claude-code-guide`), not memory. A Yes that cannot name the native feature is not absorption
- [ ] Repo-controlled data reaches the shell safely? (paths, branch names and commit messages
quoted, no `eval`, the shared extraction in `_git-target-common.sh` used where it applies —
rule `security.md`)
- [ ] Its README row matches what it does?
**Stage 2 — pressure-test (non-Keep candidates only).** Generate 1–3 hook-specific yes/no
questions that try to refute the draft verdict, each answered with one line of evidence (a file
read, a test run, a docs check). For Dissolve, the mandatory question is "Can the absorbing native
feature be named and shown in the current docs — Yes/No". Fire counts are evidence, never a
verdict: a guard that never fires may be doing its job.
| Verdict | Meaning |
|---|---|
| Keep | Wired, tested, earns its run cost, safe |
| Improve | Keep, with a named fix (add a test, move logic out of `settings.json`, tighten quoting) |
| Update | A referenced tool, flag or path changed (verified) |
| Merge into [X] | Two hooks on the same event do overlapping work; name the survivor |
| Dissolve | Claude Code does it natively now; name the feature |
| Retire | Orphaned, its retirement condition fired, or broken beyond repair |
A security No is Improve or Update at least, and its application runs `security-reviewer` on the
diff (skill `implementation-chain`, Security Review row).
## Phase 3 — Summary
Render a table: `Hook | Event | Tests | 90d | Verdict | Reason`. Close with one line: hooks found,
wired, orphaned, untested.
## Phase 4 — Report and apply
Turn every verdict into items (source `hooks`) of the report contract in skill:
`harness-stocktake` and hand them to it: one report, one approval from the author, application
on a task branch, a completion report, an ff-only merge. Called alone, this skill runs that
contract's Phases 2–5 with its own items.
- **Improve / Update / Merge**: `change` = the script diff and, when wiring changes, the
`settings.json` edit. Script edits apply with their bats test in the same commit
- **Dissolve / Retire**: `change` = removal of the script and its tests, the README row, and the
`settings.json` entry; `impact` = other hooks sourcing it, skills or rules naming it
- **Keep**: `reason` only
Every `reason` is self-contained and cites the No answers (question + one-line evidence), as in
skill-stocktake's Reason quality section.
**Ledger** `${CLAUDE_SKILL_DIR}/results.json`, updated on the apply branch in the same commit:
```json
{
"evaluated_at": "2026-10-31T00:00:00Z",
"hooks": {
"hooks/validate-bash.sh": { "verdict": "Keep", "reason": "..." }
}
}
```
Keys are paths from the ~/.claude root (`hooks/x.sh`, `scripts/hooks/x.py`), dotfiles and
`__pycache__` excluded — the set harness_lint.py `lint_stocktake_ledgers` compares against disk;
`evaluated_at` is real UTC
from `date -u +%Y-%m-%dT%H:%M:%SZ`. An item the author skipped keeps its verdict with
`"skipped": true`.
## Related
- `harness-stocktake` — runs this audit with the other stocktakes into one report
- `skill-stocktake` / `rules-stocktake` / `agent-stocktake` — the sibling audits
- `security-reviewer` — reviews a hook diff that touches the threat surface