My master sessions are long-lived on purpose. A project keeps one Claude Code conversation for days, and that's the point: no re-explaining where we were. But a conversation can't grow forever. When it reaches the model's context limit, Claude Code compacts it: the history is replaced by a summary, and the session carries on from that.
Most of the time you don't notice. The trouble is the things you would only notice by their absence.
What survives a compaction, by default
Claude Code's docs are specific about some of this. After a compaction:
- Re-read from disk: the project's
CLAUDE.mdand unscoped rules, and auto memory. These come back word for word. - Re-read, up to a limit: the five most recently read or edited files. Anything over about 5,000 tokens comes back as a path only.
- Re-injected, capped: skills the session had invoked, up to 5,000 tokens each and 25,000 in total.
- Summarised, not restored: everything else. That covers tool output, context that hooks added earlier, and the conversation itself.
What the summary is told to keep isn't documented beyond "key information". You can steer it with /compact <instructions> when you compact by hand, but an automatic compaction happens when the window is full, mid-task, with no instructions.
What that loses in practice
The rule in my lab is that the GitLab issue is the record. Decisions are written there as "Decided: …", progress is shown by labels, and sessions report on the issue, not in the chat. Anything already on an issue survives any compaction, because the session can just read it again.
The gap is everything that hasn't reached an issue yet:
- a question the session asked me that I haven't answered;
- a decision I made in passing in the chat, before anyone wrote it up;
- a promise like "I'll check the live page once this merges";
- a fact learned the hard way, such as which tool can't reach the LAN or which bundle version 404s.
A good summary keeps most of these. But "most" is the problem: the one that gets dropped is the one nobody notices until it's re-asked or quietly forgotten. With defaults, nothing in the setup is looking for them specifically.
The handover: two hooks
Claude Code has a PreCompact hook, which runs just before a compaction. It gets the session ID and the path to the transcript, but it can't add anything to the context itself. So the handover is two hooks, both pointing at one small Python script:
PreCompact handover.py pre
read the transcript since the last compaction
ask a model for a handover note
save it as ~/.claude/handovers/<session-id>.md
SessionStart (compact) handover.py post
print that session's note back into the context
Writing the note. The script reads the session's transcript, a JSONL file Claude Code keeps on disk, starting at the last compaction boundary. It sends that to claude -p on Sonnet with a fixed brief: what's in progress and the exact next step, open threads, what's waiting on me, what's waiting on other sessions, what was decided (quoting me briefly), and gotchas. It must say only what the transcript supports, and must never copy a secret value, only where the secret lives.
Reading it back. A SessionStart hook with the matcher compact fires right after the compaction and injects the note with the summary. The session reads its own handover in its own words.
A few choices that matter:
- One file per session ID. I run seventeen master sessions, and several can compact at the same time. Each session keeps its ID across restarts, so a revived session finds its own note.
- Never in a repo. The note holds whatever the conversation held, so it lives under
~/.claude, readable only by me, and is pruned after 30 days. - It never blocks a compaction.
PreCompactcan block one, which would leave a full session stuck. Every failure path in the script exits cleanly. - The child doesn't recurse. The
claude -pit starts runs without settings, tools or MCP servers, and an environment flag makes the hook exit at once if it ever fires inside it. - If the summary fails, keep the raw tail. The note then holds the last 40,000 characters of the conversation instead.
The first day
The hooks went live on 16 October, and by that evening four sessions had a handover note. Two came out as intended: short and structured, with the next step first.
The other two say "Summariser failed (You've hit your session limit)" and contain only the raw tail.
The summariser runs on the same Claude subscription as the sessions. Long sessions compact when they've done a lot of work, and that's exactly when the subscription is most likely to have hit its limit. The fallback did its job, and the session got the end of the conversation back. But a raw tail has no "waiting on Marlon" section, and anything older than the tail is gone, which is the very thing the hook exists to keep.
The fix is filed. Either retry on a different backend when the limit message appears, or add a cheap local pass to the fallback that pulls out issue references, "Decided:" lines and unanswered questions.
If you only change a few settings
- Write state where compaction can't reach. Issues, a decisions file, commit messages. Hooks are a backstop, not the record.
- Add a
PreCompact+SessionStart(compact) pair if your sessions run long. Even a script that saves the raw tail beats nothing. - Compact by hand at natural breaks, with instructions. Running
/compact keep the open questions and the next stepwhen a task finishes beats an automatic compaction mid-edit. - Keep
CLAUDE.mdlean. It's re-read after every compaction, so it costs context every time, but it's also the one place guaranteed to survive. Put rules there, not state. - Know that transcripts expire. Claude Code deletes old session transcripts after 30 days by default (
cleanupPeriodDays). If you want to go back further, raise it.
One last fragility. The script finds the last compaction by a marker in the transcript file, and that file's format is internal and undocumented. If it changes, the script doesn't break. It just reads the newest part of the whole file instead, which is a reasonable thing to fail towards.