fix(claude-code): detach SessionEnd work so CLI shutdown cannot cancel the final retain - #2869
Open
merlinr68 wants to merge 1 commit into
Open
fix(claude-code): detach SessionEnd work so CLI shutdown cannot cancel the final retain#2869merlinr68 wants to merge 1 commit into
merlinr68 wants to merge 1 commit into
Conversation
…l the final retain
Claude Code cancels SessionEnd hooks that are still in flight when its
shutdown teardown finishes. This is reliably reproducible by exiting with
Ctrl+C Ctrl+C in a project with many MCP servers (the same exit is clean in
a bare directory, and /exit is always clean):
SessionEnd hook [python3 ".../session_end.py" || ...] failed: Hook cancelled
See anthropics/claude-code#32712 and anthropics/claude-code#41577.
The hook itself is fast (~0.2s measured end-to-end against a 1.4MB
transcript), so this is not a timeout: the hook is killed at the
session-exit signal level. The damage is silent data loss — the forced
final retain never reaches the server, so any session shorter than
retainEveryNTurns turns loses its memories entirely (verified server-side:
zero SessionEnd retains arrived over 36h of real sessions).
Fix: the hook entry now does no real work. It re-execs session_end.py as a
detached child (start_new_session=True, hook_input passed via argv, guarded
by HINDSIGHT_SESSION_END_DETACHED) and returns in ~85ms. The child runs in
its own session, survives the CLI's process-group teardown, and performs
the forced final retain + daemon stop exactly as before.
Verified on Claude Code v2.1.216: the previously-failing scenario
(MCP-heavy project, real turn, Ctrl+C Ctrl+C) now exits with no hook error
AND the final retain arrives at the Hindsight server.
Claude-Session: https://claude.ai/code/session_01MTPDCvguQvm5xLKeWuCYY4
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.
Problem
Claude Code cancels
SessionEndhooks that are still in flight when its shutdown teardown finishes. With this plugin installed, every shutdown of a session in an MCP-heavy project prints:The visible error is the small part. The silent part is data loss: the cancelled hook never delivers the forced final retain, so any session shorter than
retainEveryNTurnsturns (default 10) loses its memories entirely, and longer sessions lose the tail after the last Stop-hook retain. Checking my self-hosted server logs: zero SessionEnd retains arrived over 36 hours of real sessions — the only retains that landed were mid-session Stop-hook ones.Root cause
This is not a timeout and not a plugin bug per se —
session_end.pymeasures ~0.2s end-to-end against a 1.4MB transcript, far under its 10s hook budget. Claude Code kills the hook at the session-exit signal level (Hook cancelled, as distinct fromtimed out). Known upstream behavior: anthropics/claude-code#32712, anthropics/claude-code#41577.Reproduction matrix (Claude Code v2.1.216, tmux-driven interactive sessions):
/exitHook cancelled, retain lostFix
The hook entry now does no real work. It re-execs
session_end.pyas a detached child —start_new_session=True,hook_inputpassed via argv, guarded byHINDSIGHT_SESSION_END_DETACHED— and returns in ~85ms, leaving the CLI's cancellation window nothing to kill. The child runs in its own session, survives the CLI's process-group teardown, and performs the forced final retain + daemon stop exactly as before. This is the same detach pattern the plugin already uses inprestart_daemon_background(), and the workaround recommended in anthropics/claude-code#41577.Trade-off: the child's stderr is discarded (it can outlive the terminal), so final-retain errors are no longer visible at shutdown. Given the alternative is the retain being silently killed, this seems like the right side of the trade.
Testing
tests/test_hooks.py(TestSessionEndHook): hook entry spawns exactly one detached child with the marker env + hook_input argv and does no inline network work; detached child performs the final retain from argv; detached child skips retain when there is no transcript.https://claude.ai/code/session_01MTPDCvguQvm5xLKeWuCYY4