Skip to content

fix: deliver SessionStart hook payload to model context; silence PostCompact#65

Merged
attila merged 8 commits into
mainfrom
fix/sessionstart-additionalcontext-envelope
May 22, 2026
Merged

fix: deliver SessionStart hook payload to model context; silence PostCompact#65
attila merged 8 commits into
mainfrom
fix/sessionstart-additionalcontext-envelope

Conversation

@attila

@attila attila commented May 22, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fix the long-standing bug where lore's SessionStart hook payload never reached the model context: switch from the chip-only systemMessage envelope to hookSpecificOutput.additionalContext, the channel Claude Code routes into the model's conversation as a system reminder.
  • Silence PostCompact hook output: Claude Code's hook validator rejects hookSpecificOutput for that event, and the alternate systemMessage envelope 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 next PreToolUse re-injects relevant patterns.
  • Document the harness limitation in docs/hook-pipeline-reference.md, track candidate workarounds in ROADMAP.md, and update CHANGELOG.md.

Background

Lore's SessionStart hook has emitted {"systemMessage": "..."} since the integration shipped. In Claude Code's hook protocol, systemMessage is 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 every SessionStart source (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 shape PreToolUse and PostToolUse have always used correctly, and the shape peer plugins (superpowers) use for SessionStart. This branch switches SessionStart to that envelope.

Field testing revealed that Claude Code's hook output validator rejects hookSpecificOutput specifically for PostCompact while accepting it for SessionStart. There is no envelope that delivers PostCompact output 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-demand PreToolUse injection 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 the PostCompact portion 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

Event Before After
SessionStart systemMessage chip; not in context hookSpecificOutput.additionalContext; seeds model context
PreToolUse additionalContext (unchanged) additionalContext (unchanged, now built via shared constructor)
PostToolUse additionalContext (unchanged) additionalContext (unchanged, now built via shared constructor)
PostCompact systemMessage chip; not in context (no hook output emitted); dedup-file truncation still runs as a side effect

Per-event regression assertions on hookEventName exist 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::SystemMessage variant — was applied in d6a2dc6 after PostCompact switched to Ok(None). Other findings were deferred (typed HookEventName enum, when the next event lands) or already covered by existing tests.

Test plan

  • just ci clean locally (dprint check, cargo clippy --all-targets --features test-support -- -D warnings, cargo test --features test-support)
  • Built binary tested in a live Claude Code session:
    • SessionStart: model can quote the lore intro's first sentence from initial context
    • /compact: no validator error, no terminal chip; on-demand PreToolUse injection still works
    • /clear: SessionStart fires through the new envelope and reaches the model context

Release notes

Patch release 0.4.00.4.1. CHANGELOG carries the user-facing entries: Fixed for SessionStart reaching the model, Changed for PostCompact going silent.

attila added 5 commits May 22, 2026 08:26
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.
@attila attila marked this pull request as ready for review May 22, 2026 09:21
attila added 2 commits May 22, 2026 10:24
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.
@attila attila enabled auto-merge (squash) May 22, 2026 09:27
@attila attila disabled auto-merge May 22, 2026 09:27
@attila attila enabled auto-merge (squash) May 22, 2026 09:29
@attila attila merged commit 68a0065 into main May 22, 2026
10 checks passed
@attila attila deleted the fix/sessionstart-additionalcontext-envelope branch May 22, 2026 09:31
@attila attila mentioned this pull request May 22, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant