v0.7.0 — token-burn: Cache-friendly hook ordering — put volatile lines at tail, not head

Problem

SessionStart and UserPromptSubmit hooks inject context above stable system content (CLAUDE.md, skill bodies, MEMORY.md). Several hook outputs include volatile fields that change per turn, busting CC's prompt cache.

Evidence (verified against live hooks/hooks.json — 291 LOC, 39 hooks total)

Per-event hook counts (real, not estimated):

  • SessionStart: 5 hooks
    • ensure-gitignore.sh, mcp-health-check.sh, deferred-tools-drift-warn.sh, write-active-workspace-sentinel.sh, session-start-prescan.sh
  • UserPromptSubmit: 10 hooks
    • activation-routine.sh (gated: only fires when bro in prompt or sticky-bro detected from transcript), mcp-health-check.sh, session-log-capture.sh, consultant-spawn-required.sh, roundtable-slash-detect.sh, concerns-protocol-hint.sh, push-intent-hint.sh, reonboard-intent-hint.sh, resume-intent-hint.sh, adr-required-hint.sh
  • PreToolUse: 16 hooks (matched by tool name pattern)
  • PostToolUse: 6 hooks
  • SubagentStop: 1 hook (swe-atomic-close.sh)
  • WorktreeCreate: 1 hook (worktree-create.sh)

Volatile content in injected output:

  • session-start-prescan.sh (100 LOC) emits: branch, commit count, dirty count, open issue count, pending task count, file_registry warm/cold, top-level dirs, stacks detected, last 5 commits → all volatile across turns/sessions
  • activation-routine.sh (123 LOC) emits: pending=#NN: <objective> banner — changes when issue state changes
  • mcp-health-check.sh — emits status; varies across sessions

Bro-mode gating (verified in activation-routine.sh:30-46): activation-routine only emits when bro mode is active (prompt contains bro or transcript shows sticky-bro). So 10 UserPromptSubmit hooks fire, but only some emit additionalContext on a given turn.

Plan

  1. Restructure SessionStart hook output so stable inventory (git remotes, top-level dirs, stacks-detected) prints first; volatile counts (open issues, pending tasks, file_registry warm/cold) print last.
  2. Move per-turn pending-issue banner out of UserPromptSubmit injection into a dedicated bro skill that bro reads on-demand. Avoids every-turn injection.
  3. Audit all 39 hooks for volatile-above-stable patterns. Each hook that emits additionalContext should follow the "stable prefix, volatile suffix" rule.
  4. Document the cache-friendly hook contract in docs/contributing/HOOKS.md (or create it).
  5. Compress hook output — current session-start-prescan.sh emits multiple lines per field; could compress to one tabular line.

Acceptance criteria

  • Two consecutive identical user prompts produce hook output that is byte-identical for the stable prefix ≥80%.
  • A baseline session shows ≥30% better cache-hit-rate per turn after the change (measured via subjective token-cost benchmark or any cache metric CC exposes).
  • docs/contributing/HOOKS.md exists with the cache-friendly contract.

Out of scope

  • Removing hooks entirely.
  • Restructuring CLAUDE.md cache layout (covered by #2918).

Note on source

Previous description claimed "every-turn" UserPromptSubmit injection. Verified actually 10 hooks fire per turn but activation-routine.sh (the biggest banner producer) is gated to bro-mode only. Other hooks (mcp-health-check.sh, session-log-capture.sh, *-intent-hint.sh) fire unconditionally and contribute smaller amounts. Real hook count verified at 39 across 6 event types.

Edited by Zax Shen