check-wiring covers only the analytics plugins, not the busiest hook producers #4
Notifications
Total Time Spent: 17 minutes
oleks
17 minutes
No due date set.
Dependencies
No dependencies set.
Reference: oleks/analytics-playbook#4
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
The
higroup runsinstrument check-wiringover thePLUGINSlist, which defaults to the ten analytics plugins. After skipping those that declare no hooks, it checks exactly four:(identical set on carry).
The plugins actually producing the hook load are not checked at all. From the same run's
hi/report.json, invocations per plugin:The 1st, 3rd, 4th and 6th busiest hook producers have no static dispatch proof.
Why it matters
Hook dispatch fails open. A missing or non-executable hook binary is silent at runtime — the hook simply does not run, and nothing reports it.
check-wiringexiting 0 is the only thing that positively proves the binary is present and executable.So the check is currently pointed at the plugins where a silent failure would cost least, and away from the ones where it would cost most. Worse, two of the four plugins that ARE checked (
anti-patterns,plugin-publishing) have zero rows inreport.json— wiring resolves, but they never fired in the window. The check is concentrated on plugins that barely run.This is a scoping gap in the group rather than a bug in
check-wiringitself: #2 added thehigroup, and this is about which plugins it points at now that it exists.Acceptance
Check wiring for the plugins that actually declare hooks on the host — or at minimum for the top N by observed invocation count from
report.json— rather than only for the analytics-plugin roster. The two lists barely overlap.Evidence
out/20260907-223220/hi/wiring/(4 files) andout/20260907-223220/hi/report.json; same wiring set inout/carry/20260907-192341/hi/wiring/.Plan (posted before first edit — unattended session, no interactive plan gate)
Re-derived on emmett, 2026-09-08
ls -d ~/projects/claude-plugins/*/hooks/hooks.json→ 16 plugins declare hooks:agents-routing anti-patterns anxious castdeck cluster decision-flow fleet-integration hook-instrument hooklib hyprpanel-state memory plugin-publishing session-relay spec-kit webdev worktree-disciplineout/20260907-223220/hi/wiring/→ 4 checked. So 12 hook-declaring plugins have no static dispatch proof, including all four busiest producers named in the issue. The issue's framing holds.Confirmed: this is scope, not a missing feature
collect_hialready exists and works (playbook.xsh:285). Itsreport/failures/bursts/gapssteps read the state log directly and need no plugin list. Only thecheck-wiringloop iteratesplugins, which is the--pluginsanalytics roster (DEFAULT_PLUGINS, ten names). That roster exists to say which plugins the per-plugin analytics recipes profile — it was never meant to bound a wiring check, and the two lists barely overlap. I am not adding thehigroup.Fix
Take the acceptance clause's stronger branch — "the plugins that actually declare hooks on the host" — rather than "top N by invocation", because it needs no threshold and no tie-break, and it is a strict superset of the busy producers:
ROOTwith ahooks/hooks.json.collect_hichecks the union of that roster and the passedplugins, so an explicitly-named plugin still gets itsno_hooks_declared/no_checkoutrecord and nothing regresses for a caller who passes--plugins.check_wiring_rosternaming the rule in words, so a later reader sees why the set is what it is instead of inferring it from a file count — the exact confusion that produced this issue.hi/report.json's observed per-plugin invocations against the checked set and recordcheck_wiring_unchecked_producers— any plugin that demonstrably fired hooks but got no wiring proof (e.g. it has no checkout underROOT). Today that list would have surfaced this gap by itself. This is the part that makes the next occurrence loud rather than silent.check-wiring"covers the N plugins that declare hooks" — untrue today, true after (1)–(3), and it will now also print any unchecked producers.Verification
out/**is read-only to me and is not regenerable, so I will not re-runjust playbook(967s, real load, and it would produce different numbers anyway as the transcript store rolls off). I will verify by running the roster derivation and thecheck-wiringloop directly against the live checkouts and reporting the resulting rc set here.Scope
playbook.xshandjustfileonly. Nothing underout/is read-modified, moved or deleted.Fixed and landed —
f223181+3dc175bmainat3dc175b, verified onorigin/main.Verified live, not from the collected run
out/**is unregenerable and was read-only to me, so I did not re-runjust playbook. Instead I ran the new roster derivation and the actualcheck-wiringloop against the live checkouts:--pluginsanalytics roster)All 17 exit rc=0 — no wiring failures today. The fix adds proof, not alarms.
Newly covered, never checked before:
anxious,castdeck,cluster,fleet-integration,hook-instrument,hooklib,hyprpanel-state,memory,session-relay,spec-kit,webdev,worktree-discipline— including all four busiest producers the issue names.One re-derived number disagrees with the issue, and the tracker wins
The issue says four plugins were checked. Re-derived today it would be five —
token-budgetnow declares hooks.That is not a correction to the issue; it is the point of the fix arriving as a live demonstration. I counted 16 hook-declaring plugins at 00:26 and 17 at 00:37. In between, a sibling session created
token-budget/hooks/hooks.json(commitd702d8c, oleks/emmett#681). A hardcoded roster was stale within eleven minutes of my measuring it. Deriving it from the filesystem at run time is what makes that a non-event.What landed
f223181— the roster is now every checkout under--rootwith ahooks/hooks.json, unioned with--pluginsso a caller naming a plugin still gets itsno_hooks_declared/no_checkoutrecord. Coverage gainscheck_wiring_roster, stating the rule in words so the next reader does not have to infer it from a file count — which is the exact inference that produced this issue.3dc175b— a correction to my own first commit, caught by verifying rather than by reasoning. The newcheck_wiring_unchecked_producerscross-check (plugins that demonstrably fired hooks but got no wiring proof) came back on real data with 8 phantom entries:report.json'splugincolumn is not all plugin names: hooks that ran from a git worktree are grouped under the worktree's directory name. Of its 15 distinct labels, 7 are plugins and 8 are not. So the field is now partitioned —check_wiring_unchecked_producersintersected with real checkouts (empty, as it should be), and the unrecognised labels kept incheck_wiring_unrecognised_producersand printed in SUMMARY. They are evidence of an upstream defect, not noise to drop silently: filed as kotkan/claude-plugin-hook-instrument#95.Had I shipped only the first commit, this check would have emitted 8 false findings on its very first run — a coverage check that cries wolf immediately is worse than none.
Fail-closed
observed_hook_pluginsreturnsNoneon an unreadable or wrong-shapedreport.json, and the caller recordsUNKNOWNrather than an empty list. An empty answer produced by a failed read is indistinguishable from a clean bill of health, and the clean-looking one is the wrong default for a check whose whole job is to notice a gap.Scope
playbook.xshandjustfileonly. Nothing underout/was modified, moved or deleted; no playbook run was triggered.justfilenow records thatPLUGINSno longer bounds the wiring roster, so the old assumption cannot quietly return.⏱ worked | 08-09-26 | session oleks/245e3e60 | +17min | session-total 17min (fixed and landed on main as
f223181+3dc175b) | elapsed from timelineCorrection to my own report above — landed
9b3b534kotkan/claude-plugin-hook-instrument#95 is fixed (v0.31.0,
8a517c4, verified an ancestor oforigin/main), and establishing its real root cause refuted two claims I made in this issue's result comment. Both were mine, both were wrong, and one of them was shipping a false finding on every future run.The count was 6, not 8
I listed 8 phantom entries. Two of them were never defects:
user-hooksis correct output. It is what hook-instrument's wrap mode labels a hook declared in~/.claude/settings.json—wrap.go:66andcgroup.go:107both say so outright, andinternalPlugin = "internal"is the matching cgroup component. My check would have reported it as lost attribution forever.(no-plugin-root)is a deliberate sentinel, which I had already said in the issue body and then failed to exclude in my own code.Re-derived independently against the same
out/20260907-223220run after the fix: 6 defective labels. That reproduces #95's corrected count without taking it on trust.My stated cause was wrong too
I wrote that the bad labels were project-local
.claude/hooks/*.shruns. They are not. #95's investigation read the durable logs directly — 302,199 records — and all 40 records under those labels carry synthetic session ids and/tmpfixture cwds. They are test-suite runs of a plugin whose checkout is a git worktree:CLAUDE_PLUGIN_ROOTpointed at a genuine plugin root that merely happened to be named after a branch.derivePluginNametookfilepath.Base()of it as a proxy for identity.That is the third hypothesis for this label set and the first supported by evidence. My two guesses — the cwd walk, then project-local hooks — were both wrong, and both were plausible enough to have cost someone a day.
What changed in the code
3dc175bsorted labels into "plugin checkout" and "everything else", and its SUMMARY prose asserted a cause for the second group. Two problems, one structural:(no-plugin-manifest)days after I wrote the check, and warned me that anything enumerating sentinels by name would need updating. A list would have failed silently and in the wrong direction: an unknown sentinel would be reported as a defect, so the check would cry wolf precisely when upstream did something right. The shape rule absorbed the new sentinel with no edit, verified. Only the unparenthesised wrap labels (user-hooks,internal) are named, and that set is pinned to the upstream file that defines them.Net effect on the check's output for the collected run:
unchecked real plugins: none,honest non-plugin: (no-plugin-root), user-hooks,defective: 6. Nothing underout/was modified.