feat(plugin): a PreCompact hook tells the summarizer what must survive (#3680)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 45s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m0s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 45s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m0s
CI & Build / Build & push image (push) Skipped
Spike #3680 asked whether a PreCompact hook can reach the model. Read out of the installed Claude Code build (2.1.273), the answer is yes — through a different channel than note #3679 assumed: * `hookSpecificOutput.additionalContext` is NEVER read on PreCompact. The hook-output schema has no PreCompact variant; the field is honoured for SessionStart, SubagentStart and Stop, and silently dropped here. * A PreCompact hook's STDOUT becomes `newCustomInstructions`, merged with the operator's own `/compact` instructions and passed into the prompt that writes the summary. Manual, auto and partial compaction all do this. So the hook does not interrupt the compaction — it steers the summary, which is what the next turn reads. Blocking is the thing not to do: a PreCompact block SKIPS compaction, tells the model nothing, and leaves the session running on uncompacted with no summary at all. scribe_precompact_preserve.sh names what the summary is the only copy of: Scribe record ids WITH titles, the in-progress task and its milestone, work done but not yet recorded, governing rules, and unfinished operator asks. No network and no config — what must survive is already in the conversation being summarized; the hook only says which parts are load-bearing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "scribe",
|
||||
"description": "Scribe for Claude Code: connects the scribe MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Scribe skills (using-scribe, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting), and syncs your saved Scribe Processes as skills (/scribe:sync).",
|
||||
"version": "2026.09.15.1921",
|
||||
"version": "2026.09.16.1211",
|
||||
"author": {
|
||||
"name": "Bryan Van Deusen"
|
||||
},
|
||||
|
||||
@@ -55,6 +55,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_precompact_preserve.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe plugin — PreCompact: tell the summarizer what must survive (#3680).
|
||||
#
|
||||
# WHAT THIS HOOK ACTUALLY DOES, and why it is not what the design first assumed.
|
||||
#
|
||||
# Spike #3680 asked whether a PreCompact hook can reach the model, and note
|
||||
# #3679 assumed the channel would be `hookSpecificOutput.additionalContext`
|
||||
# plus a block. Both halves of that are wrong, read out of the installed build
|
||||
# (Claude Code 2.1.273):
|
||||
#
|
||||
# * `additionalContext` is NEVER read on PreCompact. The hook-output schema
|
||||
# has no PreCompact variant at all; the field is honoured for SessionStart,
|
||||
# SubagentStart, Stop and friends, and silently dropped here.
|
||||
#
|
||||
# * A PreCompact hook's STDOUT becomes the compaction's custom instructions.
|
||||
# The handler collects every hook that exited 0 with non-empty stdout and
|
||||
# returns it as `newCustomInstructions`, which is merged with whatever the
|
||||
# operator typed after `/compact` and passed into the prompt that writes
|
||||
# the summary. Every path does this — manual, auto and partial compaction.
|
||||
#
|
||||
# So there IS a model-reaching channel, and it is better than the one designed:
|
||||
# we do not interrupt the compaction, we steer the summary it produces. The
|
||||
# summary is what the next turn reads, so text that survives into it survives
|
||||
# the compaction.
|
||||
#
|
||||
# WHAT NOT TO DO HERE: never block. A PreCompact block (exit 2, or
|
||||
# `{"decision":"block"}`) does not pause for the model and cannot tell it
|
||||
# anything — the compaction is SKIPPED, a warning goes to the operator's
|
||||
# screen, and the session continues uncompacted toward its context limit with
|
||||
# no summary at all. That failure is silent from the model's side, which is
|
||||
# exactly the outcome #3680 existed to avoid shipping. This hook exits 0 on
|
||||
# every path.
|
||||
#
|
||||
# SCOPE: in a subagent the handler discards hook stdout and keeps only a block,
|
||||
# so this steers the main session's compaction and nothing else. That is the
|
||||
# one we care about — a subagent's summary does not outlive it.
|
||||
#
|
||||
# NO NETWORK, NO CONFIG. What must be preserved is already in the conversation
|
||||
# being summarized; this hook's job is to say which parts of it are load-bearing
|
||||
# so the summarizer keeps them literally instead of compressing them away.
|
||||
# Naming the in-flight task ids from the instance would need a server round-trip
|
||||
# inside the compaction path, which is a separate, measurable question.
|
||||
#
|
||||
# PROOF THAT IT FIRED: on a manual `/compact` the handler shows the operator
|
||||
# `PreCompact [<command>] completed successfully: <stdout>`, so the hook's own
|
||||
# text is the receipt. (Auto-compaction suppresses that notification.)
|
||||
set -uo pipefail
|
||||
|
||||
# Drain the event so the caller never sees a broken pipe. Nothing in it changes
|
||||
# what we emit: the instruction is the same whether the operator typed
|
||||
# `/compact` or the session hit its limit, and it composes with any custom
|
||||
# instructions they gave, which are merged ahead of ours.
|
||||
cat >/dev/null 2>&1 || true
|
||||
|
||||
cat <<'EOF'
|
||||
Preserve the following literally in the summary — copied through, not
|
||||
paraphrased or counted:
|
||||
|
||||
- Every Scribe record the conversation refers to, by id AND title: tasks,
|
||||
issues, milestones, notes, rules, systems. A bare "#4061" is not enough; a
|
||||
record whose name is lost has to be looked up again before it can be used.
|
||||
- Which task is in progress and what its status was last set to, plus the
|
||||
milestone it sits under.
|
||||
- Work that was done but NOT yet recorded in Scribe — an edit with no work-log,
|
||||
a fix not filed as an issue, a decision not written down. Carry these over as
|
||||
outstanding; they exist nowhere else once this conversation is summarized.
|
||||
- Any rule or preference that was retrieved and still governs the work, by id
|
||||
and title.
|
||||
- Anything the operator asked for that has not been done yet, in their words.
|
||||
|
||||
Everything else here can be recovered from the repository or from Scribe. These
|
||||
cannot: they are this session's only copy.
|
||||
EOF
|
||||
exit 0
|
||||
@@ -24,9 +24,15 @@
|
||||
# fires after a compaction (SessionStart input `source` == "compact"), when
|
||||
# earlier turns have just been summarized and in-flight state is most at risk.
|
||||
# On that source we lead with a banner telling the model to reload project +
|
||||
# in-flight tasks from Scribe. (PreCompact is the wrong tool here — a host hook
|
||||
# can't make the model flush, and can't know the in-flight task ids; the durable
|
||||
# path is record-as-you-go + this post-compaction reload.)
|
||||
# in-flight tasks from Scribe. This stays the durable path: record-as-you-go
|
||||
# plus a post-compaction reload from the instance.
|
||||
#
|
||||
# The other half arrived with #3680. A host hook still cannot make the model
|
||||
# flush before a compaction — but scribe_precompact_preserve.sh steers the
|
||||
# SUMMARY, because a PreCompact hook's stdout becomes the compaction's custom
|
||||
# instructions. The two are complementary, and neither replaces the other: that
|
||||
# hook decides what survives into the summary, this one reloads from the record
|
||||
# once the summary lands.
|
||||
#
|
||||
# IMPORTANT: do NOT pass config via `${user_config.*}` substitution in a
|
||||
# shell-form hooks.json command — Claude Code rejects that outright (splicing a
|
||||
|
||||
Reference in New Issue
Block a user