fix: deliver SessionStart hook payload to model context; silence PostCompact#65
Merged
Merged
Conversation
The SessionStart hook (and PostCompact) emitted its payload via
`{"systemMessage": "..."}`, which Claude Code routes only to the
terminal UI as a transient chip. The lore intro and pinned-conventions
index have therefore been invisible to the model in every workspace,
on every SessionStart source and on every PostCompact, for the
lifetime of the integration.
Switch both handlers to the `hookSpecificOutput.additionalContext`
envelope — the same shape PreToolUse and PostToolUse already use —
so the payload lands in the model's context as a system reminder.
Drop the now-unused `HookOutput::SystemMessage` variant and collapse
`HookOutput` to a struct with a `new(event_name, context)`
constructor used at all four return sites. The user-facing terminal
chip disappears; the content was always meant for the model.
Test assertions read from `hookSpecificOutput.additionalContext`
instead of the top-level `systemMessage` key, and SessionStart /
PostCompact tests now also assert `hookEventName` so copy-paste
mistakes that would put `SessionStart` on a PostCompact payload
(silently dropped by Claude Code, reproducing this class of bug)
fail loudly.
Update the hook pipeline reference so the event table and prose match what the code now emits: SessionStart and PostCompact deliver their payload via `additionalContext`, not `systemMessage`. Add a short aside explaining why every event uses the same envelope and noting the corrected behaviour in 0.4.1. Add a CHANGELOG Fixed entry under [Unreleased] capturing the behavioural delta for the next release. Include the implementation plan that scoped this work.
Field testing surfaced that Claude Code's hook output validator rejects `hookSpecificOutput` for the PostCompact event, even though it accepts the envelope for SessionStart, PreToolUse, and PostToolUse. The prior commit's PostCompact change therefore broke `/compact` with a hook validation error. Revert PostCompact alone to the `systemMessage` envelope; SessionStart stays on `additionalContext` because that fix works as intended. Reintroduce a focused `HookOutput::SystemMessage` variant alongside `HookOutput::AdditionalContext` with named constructors (`HookOutput::additional_context(event, ctx)` and `HookOutput::system_message(ctx)`) so the per-event envelope choice is explicit at every call site. PostCompact's payload renders as a terminal chip and does not re-seed the model context after compaction — a harness limitation now documented in `docs/hook-pipeline-reference.md` and tracked as a future workaround in `ROADMAP.md`. CHANGELOG narrows the claim accordingly. Tests revert PostCompact assertions to `systemMessage`; SessionStart assertions remain on `hookSpecificOutput.additionalContext`.
Claude Code's hook output validator rejects `hookSpecificOutput` for PostCompact, and the alternate `systemMessage` envelope is pure terminal noise — never reaches the model, renders as a 30-line chip on every `/compact`. The handler now suppresses output entirely and keeps only its load-bearing side effect: truncating the per-session dedup file so the next PreToolUse re-injects patterns. Retained as an extension point. When Claude Code accepts `additionalContext` for PostCompact, or when a cleverer re-prime mechanism arrives (opportunistic re-seed on first PreToolUse after a truncated dedup file; a `lore reprime` agent-callable surface), the handler is where it plugs in. Roadmap tracks candidate workarounds. Tests: - New `hook_post_compact_produces_no_output` pins the silence - `hook_post_compact_resets_dedup_file` continues to pin the dedup truncation contract (the only externally observable behaviour) - Removed three obsolete payload-based PostCompact tests (`returns_session_context`, `re_emits_pinned_section`, the predicated universal exclusion test) — their assertion subject no longer exists Docs: - Event table marks PostCompact's output field as `(none)` - PostCompact section explains the validator constraint and the retain-as-extension-point posture - CHANGELOG splits into Fixed (SessionStart) and Changed (PostCompact silence) entries
PostCompact returns Ok(None) now, so nothing constructs the systemMessage envelope. The variant, its system_message() helper, and the doc paragraph defending its retention are unreachable code. Collapse HookOutput back to a single-shape struct with one constructor (additional_context). Aligns shipped state with the plan's Key Technical Decisions; re-adding the variant when a real chip-only event appears is a five-line change a real caller will shape better than a preserved-just-in-case alternative. Annotate additional_context() with a one-line note: tighten hookEventName from &str to a typed HookEventName enum when the next event lands. Stable today (four call sites, all literals, all test-asserted) but the typo class is exactly the bug shape this branch fixed, so flag the future tightening at the construction site.
The lore SessionStart envelope bug shipped non-functional for the integration's full life because every signal upstream of the model said "working" — exit 0, hook_success, terminal chip visible. Only a model-side acceptance prompt could prove delivery, and that wasn't part of the original integration test plan. Captures the failure mode (silent channel misroute), the diagnostic that surfaced it (acceptance prompt asking the model to quote the payload), and the prevention discipline for any host integration where the payload targets a downstream consumer. Cross-references the companion lore-patterns entry on Claude Code envelope acceptance.
…lementation notes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SessionStarthook payload never reached the model context: switch from the chip-onlysystemMessageenvelope tohookSpecificOutput.additionalContext, the channel Claude Code routes into the model's conversation as a system reminder.PostCompacthook output: Claude Code's hook validator rejectshookSpecificOutputfor that event, and the alternatesystemMessageenvelope would render as long, recurring terminal noise that never reaches the model. The handler is retained as an extension point and continues to do its load-bearing dedup-file truncation so the nextPreToolUsere-injects relevant patterns.docs/hook-pipeline-reference.md, track candidate workarounds inROADMAP.md, and update CHANGELOG.md.Background
Lore's
SessionStarthook has emitted{"systemMessage": "..."}since the integration shipped. In Claude Code's hook protocol,systemMessageis a terminal-only channel: it renders as a transient chip to the user but never enters the model's conversation context. The lore intro and the available-patterns index have therefore been invisible to the agent in every workspace, on everySessionStartsource (startup,resume,clear,compact), for the lifetime of the integration. Only the user has ever seen the payload, and only when the terminal wasn't busy drawing the welcome banner or resume notices.The working envelope is
{"hookSpecificOutput": {"hookEventName": "<event>", "additionalContext": "..."}}— the same shapePreToolUseandPostToolUsehave always used correctly, and the shape peer plugins (superpowers) use forSessionStart. This branch switchesSessionStartto that envelope.Field testing revealed that Claude Code's hook output validator rejects
hookSpecificOutputspecifically forPostCompactwhile accepting it forSessionStart. There is no envelope that deliversPostCompactoutput to the model. Rather than emit a long, recurring terminal chip with no audience, the handler now suppresses output entirely. The pinned-conventions index is unavailable after/compact; on-demandPreToolUseinjection continues to work.Full diagnosis with transcript evidence:
tmp/lore-sessionstart-context-bug.md(not committed; workspace artefact).Implementation plan:
docs/plans/2026-05-22-001-fix-sessionstart-additionalcontext-envelope-plan.md. Note the plan was committed mid-session and the field-test finding caused thePostCompactportion to diverge from the plan's original "delete SystemMessage variant" decision; the final commit realigns the shipped state with that decision after a brief detour.Behavioural delta
systemMessagechip; not in contexthookSpecificOutput.additionalContext; seeds model contextadditionalContext(unchanged)additionalContext(unchanged, now built via shared constructor)additionalContext(unchanged)additionalContext(unchanged, now built via shared constructor)systemMessagechip; not in contextPer-event regression assertions on
hookEventNameexist in the test suite for all four events.Code review
Multi-agent review (correctness, testing, maintainability, api-contract) was run on the branch. The high-confidence finding — dead
HookOutput::SystemMessagevariant — was applied ind6a2dc6afterPostCompactswitched toOk(None). Other findings were deferred (typedHookEventNameenum, when the next event lands) or already covered by existing tests.Test plan
just ciclean locally (dprint check,cargo clippy --all-targets --features test-support -- -D warnings,cargo test --features test-support)/compact: no validator error, no terminal chip; on-demandPreToolUseinjection still works/clear: SessionStart fires through the new envelope and reaches the model contextRelease notes
Patch release
0.4.0→0.4.1. CHANGELOG carries the user-facing entries:Fixedfor SessionStart reaching the model,Changedfor PostCompact going silent.