Deliver the compaction briefing through the hook that can inject it - #522
Merged
zzet merged 2 commits intoAug 9, 2026
Merged
Conversation
The PreCompact hook emitted its orientation snapshot as hookSpecificOutput.additionalContext. PreCompact has no context-injection contract in Claude Code: additionalContext is honoured for SessionStart, Setup, SubagentStart, the tool events, Stop and SubagentStop, and PreCompact's only documented output is a top-level `decision: "block"`, which blocks compaction. Its plain stdout is not context either — only UserPromptSubmit, UserPromptExpansion and SessionStart get that. So the whole briefing — graph stats, symbol history, hotspots, feedback, and the "do not re-read indexed files" instruction — was discarded. Move it to SessionStart(source="compact"), which does honour additionalContext and fires once the new window is built. The Source field was already parsed and already carried "compact"; it was just never read. The block now also names the reason it matters. Claude Code rebuilds a compacted window from the summary, the most recent exchanges, and up to five recently-read files that it re-reads from disk and re-injects as system-reminder blocks. That path invokes the Read tool directly, so no PreToolUse hook sees it and none of the redirects to graph tools apply: the agent wakes up holding raw whole-file content it never asked for. Saying so is what stops it paying for those bytes a second time. The briefing degrades to advisory-only when the bridge is unreachable rather than to silence — the advisory needs no daemon call and is the half that actually changes what the agent does next. PreCompact stays registered so hook-effectiveness telemetry still records compactions, and now emits nothing at all.
The daemon status line rendered "vv0.63.2" — every session start, on the default hook tier. StatusResponse.Version arrives already v-prefixed, and renderDaemonReadiness added its own literal "v" at all three of its daemon-state branches. renderLeanReadiness, immediately above it, already trimmed the prefix, which made the bug look half-fixed. It is not: no shipped instruction profile selects the lean tier, so that renderer never runs and every real session took the broken path. Route both through one helper. The trim has to tolerate an unprefixed value too — canonicalVersion composes the "v" through Version.String() on its happy path, but its fallback branch returns the bare version.
zzet
deleted the
fix/deliver-the-compaction-briefing-through-sessionstart
branch
August 9, 2026 16:23
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.
Investigating #70 turned up that the reporter was right about the mechanism, and that a gortex-side bug sitting underneath it was worse than the issue itself.
The PreCompact briefing never reached the model
runPreCompactemitted its orientation snapshot ashookSpecificOutput.additionalContext. PreCompact has no context-injection contract in Claude Code. Per the hooks reference,additionalContextis honoured forSessionStart,Setup,SubagentStart, the tool events,StopandSubagentStop; PreCompact's only documented output is a top-leveldecision: "block"(or exit 2), both of which block compaction. Its plain stdout is not context either — onlyUserPromptSubmit,UserPromptExpansionandSessionStartget that.So the entire briefing — graph stats, symbol history, hotspots, feedback, and the "do not re-read indexed files" instruction — was discarded. As far as I can tell it never worked.
It now ships from
SessionStart(source="compact"), which does honouradditionalContextand fires once the new window is built. TheSourcefield was already parsed and already carried"compact"; it was simply never read. Gortex registers SessionStart with no matcher, so no installer change is needed.The block now says why it matters
This is the substance of #70. Compaction rebuilds the window from the summary, the most recent exchanges, and up to five recently-read files, which Claude Code re-reads from disk and re-injects as
system-reminderblocks. That path invokes the Read tool directly, so noPreToolUsehook sees it and none of gortex's redirects to graph tools apply — the agent wakes up holding raw whole-file content it never asked for, and will happily read those files again.The briefing degrades to advisory-only when the bridge is unreachable, rather than to silence as before: the advisory costs no daemon call and is the half that actually changes what the agent does next.
PreCompact stays registered so hook-effectiveness telemetry still records compactions, and now emits nothing.
Second fix: the doubled version prefix
The daemon status line rendered
vv0.63.2on every session start — the same shape reported in #70 asvv0.41.0+29a0849.StatusResponse.Versionarrives already v-prefixed andrenderDaemonReadinessadded its own literalvat all three of its daemon-state branches.renderLeanReadinessdirectly above it already trimmed the prefix, which made this look half-fixed. It is not — no shipped instruction profile selects the lean tier, so that renderer never runs and every real session took the broken path. Both now route through one helper, tolerant of an unprefixed value becausecanonicalVersion's fallback branch returns one.Docs
docs/agents.mdlisted 4 installed hooks; the installer writes 8. Also documents why the post-compaction block hangs off SessionStart.docs/features.mdclaimed PreCompact injects the snapshot.docs/agents.md:318's "PreCompact / PostCompact — Not installed" row is not wrong and is untouched: it sits under the Codex "Intentional Codex lifecycle gaps" table.Verification
Unit tests plus an end-to-end run of the built binary against a live daemon:
SessionStart source=compactvvSessionStart source=startup(v0.63.2-39-g78a79eaf+78a79eaf, …), previously(vv0.63.2-…PreCompactgo test -race ./internal/hooks/passes andgolangci-lintreports 0 issues. The first commit was checked out standalone to confirm it compiles, since the two fixes were split out of one file.Not addressed here
Three defects found during the investigation, deliberately left out of this PR:
HookTierLeanis unreachable — no shipped profile selects it, sorenderLeanReadinessis dead code. Wiring bug or reserved tier is a product call.trackedRepoForPath(session_paths.go:210) compares paths byte-exactly, which fails on any case-insensitive filesystem — macOS as much as Windows — while a case-folding helper exists and is used elsewhere. Live enforcement hole.claudecode/hooks.go:131) is unquoted, so an install path containing a space yields a command the shell splits and no hook fires. Latent, every OS.Refs #70.