Skip to content

perf(hooks): skip node-notice and probe SessionStart rows on compaction #6251

Description

@kyle-sexton

Part of #6244

Problem

Every SessionStart row in the fleet has no matcher. SessionStart fires with source startup, resume, clear, compact or fork (code.claude.com/docs/en/hooks.md:308,1132), so with no matcher every row runs again on each of those. On origin/main there are 39 SessionStart rows across the hook plugins (counted over every hook in every group):

  • 25 shell-form node-notice rows, one per hook plugin (for example plugins/eol-normalizer/hooks/hooks.json), each running sh lib/prerequisites.sh node-notice ..., which is two processes (the shell Claude Code starts, then sh).
  • 8 exec-form prerequisites.mjs probe rows in the formatter plugins (actionlint, bash-format, biome-format, go-format, markdown-format, powershell-format, ruff-format, typos-format), each a node process.
  • 6 others: three Python dependency installers (filed separately), harness-ops' gated event log, session-flow observer-arm.sh, testing's judge start.

On compaction both notices are already latched for the session, so the re-fire shows nothing new and only spends processes:

  • node-notice keeps a latch keyed by session id in the temp directory (lib/prerequisites.sh:15-40), and exits at once when command -v node succeeds (:21).
  • probe keeps a once-per-session latch in ${CLAUDE_PLUGIN_DATA}/skip-notices/ (docs/conventions/prerequisites/README.md:121).

If compaction keeps the session id (inferred, not probed; see Evidence), then on compact every one of these 33 rows starts its processes and exits without output. Long sessions compact many times.

Evidence

Verified this pass (origin/main 26f98e156; hooks docs fetched 2026-10-04):

  • Inventory: jq over every plugins/*/hooks/hooks.json SessionStart group and every hook in it: 39 rows, 25 node-notice, 8 probe, none with a matcher.
  • hooks.md:1118: on startup, resume at launch, and /clear, SessionStart hooks run in the background, "Claude's first response still waits for the hooks to finish". compact is not in that sentence.
  • /clear starts a new conversation (sessions.md:232; SessionEnd fires with reason clear, hooks.md:3364), so a notice latched by session id likely fires again after /clear, and Claude's context, which /clear emptied, gets the warning back. That is a real effect, so clear stays in the matcher.
  • Inferred, not probed: that compact keeps the session id and /clear changes it. hooks.md:588 says SessionStart "fires again later in the session, after /clear or a compaction", which leaves the /clear half open. Keeping clear in the matcher is the safe choice either way. Before landing, confirm that the session_id in a compact SessionStart payload equals the session's startup one (a claude --debug-file run is enough). If it differs, excluding compact drops a re-notice, and the change needs the owner's call.

From the 2026-10-04 Windows audit (Claude Code 2.1.289; E2 aggregate with cause inferred, E3 inference):

  • About 75 to 80 processes start on every SessionStart (startup, resume, /clear and compaction), 46 of them only confirming node exists (E3, from the hook inventory).
  • About 4.6 s from settings load to the last SessionStart hook result in one startup debug log (E2).
  • Spawn cost on that host: node 95 ms, Git Bash 53 ms (MEDIUM).

The item proposed matcher: "startup". That would also drop resume and fork, which start a new process where a missing tool should be reported, and clear, see above. Not adopted.

Proposed approach

Give the 25 node-notice rows and the 8 probe rows "matcher": "startup|resume|clear|fork", which excludes only compact.

  • The node-notice row text is pinned as one canonical polyglot by scripts/node-notice-rows.test.sh; extend the test to pin the matcher too. The testing copy is generated by plugins/testing/scripts/gen-hook-filters.sh.
  • Update docs/conventions/prerequisites/README.md ("Hook notices" table, the node-notice row at :145, and the probe row) to state the matcher and why compact is excluded.
  • Leave the other six rows alone here: the Python installers are covered by the sibling issue for fix 4, and observer-arm, judge start and the event log have their own per-session semantics.

Alternatives considered:

  • One combined node check for the whole fleet: rejected. Each plugin installs on its own and must report its own missing node; the shared latch already prints one notice per session (docs/conventions/prerequisites/README.md:145).
  • startup only (the item's proposal): drops the post-/clear re-notice and resume/fork checks; see Evidence.

Acceptance criteria

  • A recorded check (debug log or payload capture) shows that a compact SessionStart carries the same session_id as the session's startup. If it does not, stop and ask the owner before excluding compact.
  • All 25 node-notice rows and all 8 probe rows carry "matcher": "startup|resume|clear|fork"; no other SessionStart row changes.
  • scripts/node-notice-rows.test.sh fails when a node-notice row lacks that matcher, and passes on the updated tree.
  • plugins/testing/scripts/gen-hook-filters.sh emits the matcher, and its test (gen-hook-filters.test.sh) passes.
  • The prerequisites convention doc states the matcher and the reason compact is excluded.
  • Each touched plugin carries the version bump and CHANGELOG line the repo's rules require.

Constraints and gotchas

Context

Source: local handoff item 20261004-150216-claude-perf-audit-plugin-fixes.md, fix 7. Related: #6130 and #6131 (node-notice row), #4240 (prerequisites check), #4373 (hook fan-out container). The Python dependency SessionStart rows are fix 4's issue (#6250).

Activity

  1. added
    priority: lowNice-to-have, cosmetic, or speculative; opportunistic.
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    work-class: mechanicalDeterministic, trivially reversible maintenance: dependency bumps, lint/format, sync.
    on Oct 4, 2026
  2. github-actions commented on Oct 4, 2026

    @github-actions
    Contributor

    Thanks for the detailed writeup. Understood: 25 node-notice rows and 8 probe rows fire on every SessionStart including compact, even though both already latch per-session and produce no new output on a repeat fire — this adds matcher startup|resume|clear|fork to those 33 rows to skip the wasted compact re-fire, while keeping startup, resume, clear, and fork covered for their legitimate re-notice cases. The proposal, evidence, and acceptance criteria are thorough, including the one open item flagged in the issue itself: confirming that a compact SessionStart payload's session_id matches the session's startup id before landing the change. No related open duplicates found (checked #6244, #6250, #6130, #6131).

    Automated triage. A maintainer reviews it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.priority: lowNice-to-have, cosmetic, or speculative; opportunistic.work-class: mechanicalDeterministic, trivially reversible maintenance: dependency bumps, lint/format, sync.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions