updating-hooks-dry
The fleet hook tree grows every time codifying-disciplines lands a new enforcer — and growth invites drift: two hooks that copy-paste the same logic instead of sharing a _shared/ helper, a _shared/ export nobody imports anymore, a lint rule and a hook both catching the identical AST shape, a 400-line hook doing one job its 80-line siblings do. This skill finds that bloat and writes a plan. It is read-only and plan-only by design: it applies nothing and opens no PR — a consolidation is a judgment call a human makes from the report. The one mechanical, safe gate — dead _shared/ exports — is already a hard check (shared-hook-helpers-are-used.mts); this skill is the broader, advisory companion.
When to use
- Periodically (the operator runs it; not a blocking gate), or after a burst of new hooks from
codifying-disciplines.
- When the hook tree "feels" repetitive and you want evidence + a consolidation plan before refactoring.
What it does NOT do
- Apply changes. It writes a report; a human (or a follow-up
refactor-cleaner run) executes. No Edit/Write to hook source, no commits.
- Open a PR. Plan-only by operator directive.
- Re-litigate the hard gates. Dead
_shared/ exports already fail check --all via shared-hook-helpers-are-used.mts; guard/reminder overlap is already checked by hooks-have-no-guard-nudge-overlap.mts. This skill SURFACES candidates those gates don't (near-duplicate logic, KISS smells, subsuming lint selectors) and ranks everything for a human.
Inventory first (inline, before the Workflow)
Scout the surface so the fan-out has a work-list:
- List hook dirs:
.claude/hooks/fleet/*/ and .claude/hooks/repo/*/ (exclude _shared/).
- List
_shared/ exports: rg '^export (async )?function|^export const|^export interface|^export type' .claude/hooks/fleet/_shared/.
- List lint rules:
.config/fleet/oxlint-plugin/fleet/*/.
- Run the existing detectors as ground truth (they never block here — just data):
node scripts/fleet/check/shared-hook-helpers-are-used.mts — dead _shared/ exports (advisory).
node scripts/fleet/check/hooks-have-no-guard-nudge-overlap.mts — known guard/reminder collisions.
The sweep (Workflow)
Run as a Workflow (sanctioned opt-in; pass the inventory as args). Four read-only scanner dimensions in parallel, then an adversarial verify, then synthesis. Each scanner uses agentType: 'Explore' (read-only) and returns a structured finding list.
phase('Scan') — four parallel scanners, each over the hook + lint-rule tree:
- Copy-paste clusters — hooks whose decision logic is near-identical (same parse → same match → same emit shape) and should absorb a
_shared/ helper. Compare STRUCTURE — the AST shape via _shared/shell-command.mts concepts — not just text. Schema per finding: { kind: 'copy-paste', members: [file:line], sharedHelperProposed, evidence }.
- Dead
_shared/ exports — start from shared-hook-helpers-are-used.mts output; for each candidate, confirm whether it's genuinely unused or consumed out-of-tree — the check is advisory precisely because some _shared/ exports are consumed by wheelhouse-root or sibling repos. Schema: { kind: 'dead-export', symbol, file, confirmedUnused: bool, evidence }.
- Overlapping enforcers — two enforcers catching the same shape: a lint rule + a hook for an identical AST pattern where one suffices, or two lint rules with subsuming selectors. Schema:
{ kind: 'overlap', enforcers: [name], subsumes, evidence }.
- KISS smells — a hook/rule far longer than its siblings doing one job; raw regex on a command line where the
_shared/ AST parser exists (the no-hook-cmd-regex concern); a hook reimplementing a _shared/ helper inline. Schema: { kind: 'kiss', file, smell, siblingNorm, evidence }.
phase('Verify') — adversarial pass: per finding, a skeptic tries to REFUTE it — two guards that look similar but guard genuinely different surfaces are NOT a duplicate (e.g. a PreToolUse edit-guard vs a Stop reminder for related-but-distinct concerns the overlap check already knows are fine); a _shared/ export "unused" in-tree may be consumed by wheelhouse-root. Drop a finding unless the skeptic confirms it's a real consolidation opportunity. Default to refuted when uncertain.
- Synthesize — a final
agent() writes the ranked report: highest-leverage consolidations first — a _shared/ helper that would absorb 4 hooks beats a one-off — each with evidence (file:line), the proposed consolidation, and a concrete diff sketch.
Return { report, findingCount, byKind }.
Output
Write the report to .claude/reports/hooks-dry-sweep-<YYYY-MM-DD>.md (untracked — the fleet .gitignore excludes /.claude/*; never write it to a committable path, the report-location-guard enforces this). The report is the deliverable. Apply nothing.
Report shape:
- Summary — finding count by kind; the single highest-leverage consolidation.
- Per cluster — kind, members (
file:line), the proposed _shared/ helper or merge, a diff sketch, and the blast radius (how many hooks it touches → cascade scope).
- No silent caps — if the scan bounded coverage (sampled, top-N), say so. A silent truncation reads as "swept everything" when it didn't.
Relationship to the hard gates
This skill is the advisory wide net; the deterministic gates are the safety floor. Dead _shared/ exports → shared-hook-helpers-are-used.mts (advisory check). Guard/reminder one-surface-per-concern → hooks-have-no-guard-nudge-overlap.mts (hard gate). Hook-registry currency → hook-registry-is-current.mts. When this skill finds a pattern worth enforcing deterministically, that itself is a codifying-disciplines candidate — promote it to a check rather than re-running the sweep to find it again.
1---2name: updating-hooks-dry3description: Read-only DRY sweep of fleet hooks and oxlint rules for overlap and consolidation.4---56# updating-hooks-dry78The fleet hook tree grows every time `codifying-disciplines` lands a new enforcer — and growth invites drift: two hooks that copy-paste the same logic instead of sharing a `_shared/` helper, a `_shared/` export nobody imports anymore, a lint rule and a hook both catching the identical AST shape, a 400-line hook doing one job its 80-line siblings do. This skill **finds** that bloat and writes a plan. It is **read-only and plan-only by design**: it applies nothing and opens no PR — a consolidation is a judgment call a human makes from the report. The one mechanical, safe gate — dead `_shared/` exports — is already a hard check (`shared-hook-helpers-are-used.mts`); this skill is the broader, advisory companion.910## When to use1112- Periodically (the operator runs it; not a blocking gate), or after a burst of new hooks from `codifying-disciplines`.13- When the hook tree "feels" repetitive and you want evidence + a consolidation plan before refactoring.1415## What it does NOT do1617- **Apply changes.** It writes a report; a human (or a follow-up `refactor-cleaner` run) executes. No `Edit`/`Write` to hook source, no commits.18- **Open a PR.** Plan-only by operator directive.19- **Re-litigate the hard gates.** Dead `_shared/` exports already fail `check --all` via `shared-hook-helpers-are-used.mts`; guard/reminder overlap is already checked by `hooks-have-no-guard-nudge-overlap.mts`. This skill SURFACES candidates those gates don't (near-duplicate logic, KISS smells, subsuming lint selectors) and ranks everything for a human.2021## Inventory first (inline, before the Workflow)2223Scout the surface so the fan-out has a work-list:24251. List hook dirs: `.claude/hooks/fleet/*/` and `.claude/hooks/repo/*/` (exclude `_shared/`).262. List `_shared/` exports: `rg '^export (async )?function|^export const|^export interface|^export type' .claude/hooks/fleet/_shared/`.273. List lint rules: `.config/fleet/oxlint-plugin/fleet/*/`.284. Run the existing detectors as ground truth (they never block here — just data):29 - `node scripts/fleet/check/shared-hook-helpers-are-used.mts` — dead `_shared/` exports (advisory).30 - `node scripts/fleet/check/hooks-have-no-guard-nudge-overlap.mts` — known guard/reminder collisions.3132## The sweep (Workflow)3334Run as a **`Workflow`** (sanctioned opt-in; pass the inventory as `args`). Four read-only scanner dimensions in parallel, then an adversarial verify, then synthesis. Each scanner uses `agentType: 'Explore'` (read-only) and returns a structured finding list.35361. **`phase('Scan')` — four parallel scanners**, each over the hook + lint-rule tree:37 - **Copy-paste clusters** — hooks whose decision logic is near-identical (same parse → same match → same emit shape) and should absorb a `_shared/` helper. Compare STRUCTURE — the AST shape via `_shared/shell-command.mts` concepts — not just text. Schema per finding: `{ kind: 'copy-paste', members: [file:line], sharedHelperProposed, evidence }`.38 - **Dead `_shared/` exports** — start from `shared-hook-helpers-are-used.mts` output; for each candidate, confirm whether it's genuinely unused or consumed out-of-tree — the check is advisory precisely because some `_shared/` exports are consumed by wheelhouse-root or sibling repos. Schema: `{ kind: 'dead-export', symbol, file, confirmedUnused: bool, evidence }`.39 - **Overlapping enforcers** — two enforcers catching the same shape: a lint rule + a hook for an identical AST pattern where one suffices, or two lint rules with subsuming selectors. Schema: `{ kind: 'overlap', enforcers: [name], subsumes, evidence }`.40 - **KISS smells** — a hook/rule far longer than its siblings doing one job; raw regex on a command line where the `_shared/` AST parser exists (the `no-hook-cmd-regex` concern); a hook reimplementing a `_shared/` helper inline. Schema: `{ kind: 'kiss', file, smell, siblingNorm, evidence }`.412. **`phase('Verify')` — adversarial pass**: per finding, a skeptic tries to REFUTE it — two guards that look similar but guard genuinely different surfaces are NOT a duplicate (e.g. a PreToolUse edit-guard vs a Stop reminder for related-but-distinct concerns the overlap check already knows are fine); a `_shared/` export "unused" in-tree may be consumed by wheelhouse-root. Drop a finding unless the skeptic confirms it's a real consolidation opportunity. Default to refuted when uncertain.423. **Synthesize** — a final `agent()` writes the ranked report: highest-leverage consolidations first — a `_shared/` helper that would absorb 4 hooks beats a one-off — each with evidence (`file:line`), the proposed consolidation, and a concrete diff sketch.4344Return `{ report, findingCount, byKind }`.4546## Output4748Write the report to **`.claude/reports/hooks-dry-sweep-<YYYY-MM-DD>.md`** (untracked — the fleet `.gitignore` excludes `/.claude/*`; never write it to a committable path, the `report-location-guard` enforces this). The report is the deliverable. Apply nothing.4950Report shape:5152- **Summary** — finding count by kind; the single highest-leverage consolidation.53- **Per cluster** — kind, members (`file:line`), the proposed `_shared/` helper or merge, a diff sketch, and the blast radius (how many hooks it touches → cascade scope).54- **No silent caps** — if the scan bounded coverage (sampled, top-N), say so. A silent truncation reads as "swept everything" when it didn't.5556## Relationship to the hard gates5758This skill is the advisory wide net; the deterministic gates are the safety floor. Dead `_shared/` exports → `shared-hook-helpers-are-used.mts` (advisory check). Guard/reminder one-surface-per-concern → `hooks-have-no-guard-nudge-overlap.mts` (hard gate). Hook-registry currency → `hook-registry-is-current.mts`. When this skill finds a pattern worth enforcing deterministically, that itself is a `codifying-disciplines` candidate — promote it to a check rather than re-running the sweep to find it again.