check-wiring covers only the analytics plugins, not the busiest hook producers #4

Closed
opened 2026-09-07 22:59:58 +03:00 by oleks · 4 comments
Owner

What

The hi group runs instrument check-wiring over the PLUGINS list, which defaults to the ten analytics plugins. After skipping those that declare no hooks, it checks exactly four:

out/20260907-223220/hi/wiring/
  agents-routing.json  anti-patterns.json  decision-flow.json  plugin-publishing.json

(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:

plugin invocations wiring checked
worktree-discipline 155,627 no
decision-flow 90,538 yes
hyprpanel-state 27,136 no
spec-kit 7,301 no
agents-routing 5,418 yes
memory 5,004 no
cluster 1,404 no

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-wiring exiting 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 in report.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-wiring itself: #2 added the hi group, 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) and out/20260907-223220/hi/report.json; same wiring set in out/carry/20260907-192341/hi/wiring/.

## What The `hi` group runs `instrument check-wiring` over the `PLUGINS` list, which defaults to the ten analytics plugins. After skipping those that declare no hooks, it checks exactly **four**: out/20260907-223220/hi/wiring/ agents-routing.json anti-patterns.json decision-flow.json plugin-publishing.json (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: | plugin | invocations | wiring checked | |---|---:|---| | worktree-discipline | 155,627 | no | | decision-flow | 90,538 | yes | | hyprpanel-state | 27,136 | no | | spec-kit | 7,301 | no | | agents-routing | 5,418 | yes | | memory | 5,004 | no | | cluster | 1,404 | no | 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-wiring` exiting 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** in `report.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-wiring` itself: #2 added the `hi` group, 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) and `out/20260907-223220/hi/report.json`; same wiring set in `out/carry/20260907-192341/hi/wiring/`.
oleks added the bug label 2026-09-07 22:59:58 +03:00
Author
Owner

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.json16 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-discipline

out/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_hi already exists and works (playbook.xsh:285). Its report/failures/bursts/gaps steps read the state log directly and need no plugin list. Only the check-wiring loop iterates plugins, which is the --plugins analytics 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 the hi group.

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:

  1. New helper deriving the wiring roster from the filesystem: every directory under ROOT with a hooks/hooks.json.
  2. collect_hi checks the union of that roster and the passed plugins, so an explicitly-named plugin still gets its no_hooks_declared / no_checkout record and nothing regresses for a caller who passes --plugins.
  3. Coverage gains check_wiring_roster naming 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.
  4. Close the loop: cross-check hi/report.json's observed per-plugin invocations against the checked set and record check_wiring_unchecked_producers — any plugin that demonstrably fired hooks but got no wiring proof (e.g. it has no checkout under ROOT). Today that list would have surfaced this gap by itself. This is the part that makes the next occurrence loud rather than silent.
  5. Update the SUMMARY prose, which currently asserts 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-run just 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 the check-wiring loop directly against the live checkouts and reporting the resulting rc set here.

Scope

playbook.xsh and justfile only. Nothing under out/ is read-modified, moved or deleted.

## 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-discipline` `out/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_hi` already exists and works (playbook.xsh:285). Its `report`/`failures`/`bursts`/`gaps` steps read the state log directly and need no plugin list. Only the `check-wiring` loop iterates `plugins`, which is the `--plugins` analytics 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 the `hi` group. ### 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: 1. New helper deriving the wiring roster from the filesystem: every directory under `ROOT` with a `hooks/hooks.json`. 2. `collect_hi` checks the **union** of that roster and the passed `plugins`, so an explicitly-named plugin still gets its `no_hooks_declared` / `no_checkout` record and nothing regresses for a caller who passes `--plugins`. 3. Coverage gains `check_wiring_roster` naming 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. 4. **Close the loop:** cross-check `hi/report.json`'s observed per-plugin invocations against the checked set and record `check_wiring_unchecked_producers` — any plugin that demonstrably fired hooks but got no wiring proof (e.g. it has no checkout under `ROOT`). Today that list would have surfaced this gap by itself. This is the part that makes the next occurrence loud rather than silent. 5. Update the SUMMARY prose, which currently asserts `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-run `just 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 the `check-wiring` loop directly against the live checkouts and reporting the resulting rc set here. ### Scope `playbook.xsh` and `justfile` only. Nothing under `out/` is read-modified, moved or deleted.
oleks added the agent/wip label 2026-09-08 00:26:26 +03:00
oleks added this to the analytics-playbook-board project 2026-09-08 00:26:27 +03:00
oleks closed this issue 2026-09-08 00:40:31 +03:00
Author
Owner

Fixed and landed — f223181 + 3dc175b

main at 3dc175b, verified on origin/main.

Verified live, not from the collected run

out/** is unregenerable and was read-only to me, so I did not re-run just playbook. Instead I ran the new roster derivation and the actual check-wiring loop against the live checkouts:

plugins checked
before (--plugins analytics roster) 5
after (derived from the filesystem) 17

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 fivetoken-budget now 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 (commit d702d8c, 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 --root with a hooks/hooks.json, unioned with --plugins so a caller naming a plugin still gets its no_hooks_declared / no_checkout record. Coverage gains check_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 new check_wiring_unchecked_producers cross-check (plugins that demonstrably fired hooks but got no wiring proof) came back on real data with 8 phantom entries:

(no-plugin-root)  a3bc7a75-fix-232-builder-path  de851442-failclosed-reap-173-147
user-hooks  worktrees  wtd-163-push-gate-tree  wtd-policy-reach-164  wtd170-arch-mismatch

report.json's plugin column 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_producers intersected with real checkouts (empty, as it should be), and the unrecognised labels kept in check_wiring_unrecognised_producers and 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_plugins returns None on an unreadable or wrong-shaped report.json, and the caller records UNKNOWN rather 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.xsh and justfile only. Nothing under out/ was modified, moved or deleted; no playbook run was triggered. justfile now records that PLUGINS no longer bounds the wiring roster, so the old assumption cannot quietly return.

## Fixed and landed — `f223181` + `3dc175b` `main` at `3dc175b`, verified on `origin/main`. ### Verified live, not from the collected run `out/**` is unregenerable and was read-only to me, so I did not re-run `just playbook`. Instead I ran the new roster derivation and the actual `check-wiring` loop against the live checkouts: | | plugins checked | |---|---:| | before (`--plugins` analytics roster) | **5** | | after (derived from the filesystem) | **17** | 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-budget` now 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` (commit `d702d8c`, 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 `--root` with a `hooks/hooks.json`, **unioned** with `--plugins` so a caller naming a plugin still gets its `no_hooks_declared` / `no_checkout` record. Coverage gains `check_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 new `check_wiring_unchecked_producers` cross-check (plugins that demonstrably fired hooks but got no wiring proof) came back on real data with **8 phantom entries**: (no-plugin-root) a3bc7a75-fix-232-builder-path de851442-failclosed-reap-173-147 user-hooks worktrees wtd-163-push-gate-tree wtd-policy-reach-164 wtd170-arch-mismatch `report.json`'s `plugin` column 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_producers` intersected with real checkouts (**empty**, as it should be), and the unrecognised labels kept in `check_wiring_unrecognised_producers` and 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_plugins` returns `None` on an unreadable or wrong-shaped `report.json`, and the caller records `UNKNOWN` rather 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.xsh` and `justfile` only. Nothing under `out/` was modified, moved or deleted; no playbook run was triggered. `justfile` now records that `PLUGINS` no longer bounds the wiring roster, so the old assumption cannot quietly return.
oleks added spent time 17 minutes 2026-09-08 00:42:50 +03:00
Author
Owner

⏱ worked | 08-09-26 | session oleks/245e3e60 | +17min | session-total 17min (fixed and landed on main as f223181 + 3dc175b) | elapsed from timeline

⏱ worked | 08-09-26 | session oleks/245e3e60 | +17min | session-total 17min (fixed and landed on main as f223181 + 3dc175b) | elapsed from timeline
oleks added agent/worked:oleks/245e3e60 and removed agent/wip labels 2026-09-08 00:42:51 +03:00
Author
Owner

Correction to my own report above — landed 9b3b534

kotkan/claude-plugin-hook-instrument#95 is fixed (v0.31.0, 8a517c4, verified an ancestor of origin/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-hooks is correct output. It is what hook-instrument's wrap mode labels a hook declared in ~/.claude/settings.jsonwrap.go:66 and cgroup.go:107 both say so outright, and internalPlugin = "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-223220 run 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/*.sh runs. 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 /tmp fixture cwds. They are test-suite runs of a plugin whose checkout is a git worktree: CLAUDE_PLUGIN_ROOT pointed at a genuine plugin root that merely happened to be named after a branch. derivePluginName took filepath.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

3dc175b sorted labels into "plugin checkout" and "everything else", and its SUMMARY prose asserted a cause for the second group. Two problems, one structural:

  1. A hardcoded explanation outlives the mechanism it describes. The worktree-basename cause is fixed as of v0.31.0; my prose would have gone on confidently attributing every future unrecognised label to it. The line no longer names a cause — it says the owner was lost upstream and is worth investigating, which stays true whatever the next cause is.
  2. Honest sentinels are now matched by SHAPE — parenthesised — not by an enumerated list. This is the part worth keeping. #95 added (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 under out/ was modified.

## Correction to my own report above — landed `9b3b534` kotkan/claude-plugin-hook-instrument#95 is fixed (v0.31.0, `8a517c4`, verified an ancestor of `origin/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-hooks` is correct output.** It is what hook-instrument's *wrap* mode labels a hook declared in `~/.claude/settings.json` — `wrap.go:66` and `cgroup.go:107` both say so outright, and `internalPlugin = "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-223220` run 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/*.sh` runs. 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 `/tmp` fixture cwds. They are **test-suite runs of a plugin whose checkout is a git worktree**: `CLAUDE_PLUGIN_ROOT` pointed at a genuine plugin root that merely happened to be *named after a branch*. `derivePluginName` took `filepath.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 `3dc175b` sorted labels into "plugin checkout" and "everything else", and its SUMMARY prose *asserted a cause* for the second group. Two problems, one structural: 1. **A hardcoded explanation outlives the mechanism it describes.** The worktree-basename cause is fixed as of v0.31.0; my prose would have gone on confidently attributing every future unrecognised label to it. The line no longer names a cause — it says the owner was lost upstream and is worth investigating, which stays true whatever the next cause is. 2. **Honest sentinels are now matched by SHAPE — parenthesised — not by an enumerated list.** This is the part worth keeping. #95 added `(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 under `out/` was modified.
Sign in to join this conversation.