简体中文 · 日本語 · 한국어 · Español · Français · فارسی
Claude Code compacts a long session automatically: it deletes most of your conversation, replaces it with a summary, and carries on. You usually notice because the model starts re-asking things you settled two hours ago.
This repo takes the other approach. It measures how full the session really is, and when the end is genuinely close it exports the whole session to disk and continues in a fresh one that reads the parent transcript in full before doing anything else. Nothing is summarised away, and the chain of sessions stays visible and resumable.
It also diagnoses a configuration trap worth knowing about even if you never install any of this — see The clamp.
Requires Python 3.8+ and Claude Code. Nothing else; no dependencies to install.
git clone https://github.com/IRDcode/claude-code-session-handoff
cd claude-code-session-handoff
python install.py --dry-run # see every change first
python install.pyThen restart Claude Code and check what it found:
python ~/.claude/skills/long-session-handoff/scripts/session_weight.py --explainThe default install does not change how Claude Code manages context. Auto-compaction stays exactly as it is; the guard simply hands off before it can fire. To remove everything:
python install.py --uninstallsettings.json is backed up before it is touched, existing hooks and status
lines are left alone, and installing twice is a no-op.
| path | what it is |
|---|---|
~/.claude/skills/long-session-handoff/ |
the procedure the model follows, plus three scripts |
~/.claude/hooks/session-weight-watch.py |
the detector, on four events |
~/.claude/hooks/statusline-weight.py |
weight on the status line, every render |
~/.claude/runtime/ |
anti-nag state, a log, and a measurement cache |
~/.claude/handoffs/ |
the exports, and chains.json linking parent to child |
Four hook events are registered — UserPromptSubmit, SessionStart,
PreCompact, PostCompact. Your existing hooks on those events are preserved.
| default Claude Code | with this installed | |
|---|---|---|
| when the session fills | auto-compaction fires; most of the conversation is discarded and replaced by a summary | you are offered a handoff first, well before that point |
| what the next session knows | whatever the summary captured, written by the agent that was already losing track | the parent's own transcript, read in full and verified by counts |
| history of dropped context | unreferenced in the live session | recovered from disk into 05-dropped-context.md |
| how full is it, really? | /context shows % of the window |
the status line shows % of the wall that actually ends the session |
| finding the continuation later | scroll /resume |
chains.json records parent, child, weight at migration, and whether the read was verified |
The status line looks like this:
Opus 5 | ████████░░ 85% 830k/977k | 696t 402tc 6.1h | HANDOFF DUE (5) | no-compact
Percent of the wall, not of the window. Those differ, sometimes by a factor of five, which is the whole point of the next section.
Worth reading even if you install nothing.
Claude Code has two different points at which a session ends:
compaction fires at window − reply reserve (~20k) − summary buffer (~13k)
sending is refused at ceiling − reply reserve (~20k) − margin (~3k)
The first applies when auto-compaction is on, the second when it is off. So a 200,000-token window compacts at roughly 167,000.
Here is the trap: autoCompactWindow in settings.json is clamped to the
model ceiling, silently. Ask for 1,000,000 against a 200,000 ceiling and you
get 200,000 — with nothing in the UI saying so. A session configured for a
million tokens gets compacted at 167,000, three times in a row, while ~830,000
tokens of paid-for context sit unused.
That is not hypothetical. It is where this repo came from: three compactions at
preTokens 167,398 / 167,071 / 166,904, against a settings file that said
autoCompactWindow: 1000000.
--explain shows you which case you are in:
WINDOW
client reported 1,000,000
ceiling 1,000,000 (source DISABLE_COMPACT+CLAUDE_CODE_MAX_CONTEXT_TOKENS)
resolved 1,000,000 (source settings)
WALL -- the token count past which no more work happens here
1,000,000 ceiling - 20,000 reply reserve - 3,000 margin
= 977,000 then SENDING IS REFUSED (no summary; a handoff is the only exit)
If it says settings CLAMPED to …, your window setting is being floored.
One configuration escapes the clamp: DISABLE_COMPACT=1 together with
CLAUDE_CODE_MAX_CONTEXT_TOKENS. The installer can set that for you, but it
asks first and tells you the cost, because it also disables manual
/compact:
python install.py --disable-compact --window 1000000With that set, the session no longer ends in a summary — it ends in a refusal to send. That is a real trade. A refusal is survivable: you hand off and carry on. A silently destroyed history is not. But if you ignore every prompt all the way to the wall, that session stops accepting turns, and you should know that going in. The guard fires at 85%, leaving ~147,000 tokens of room, so in practice it does not get there.
Skip this flag if you would rather keep /compact. The handoff still works.
Claude Code versions. Nothing here patches or wraps Claude Code. It reads two documented interfaces — the hook stdin/stdout contract and the status-line payload — and parses the JSONL transcripts the client already writes. That is why it survives updates that would break a tool built on internals.
Where an exact number is needed, it is taken from evidence rather than asserted:
- The window comes from
context_window_size, which the client reports about itself on every status-line render. - The compaction trigger, if the session has ever been compacted, comes from the
preTokensrecorded in the transcript at that moment — the trigger observed, not computed.score()prefers it over its own arithmetic and sayscorrected from observed preTokenswhen it does. - Only with neither of those does it fall back to the reserve arithmetic, and
--explainshows every input so drift is visible rather than silent.
If a future release moves the reserves, a session that gets compacted once teaches the tool the new trigger.
Older Claude Code versions. The four hook events and the status line have been
stable for many releases. If a hook event is missing on your version, that hook
never fires and the rest still works — the detector is additive, not a
replacement. --disable-compact is the one part that depends on specific setting
names; --explain will tell you if it had no effect.
Operating systems. Pure Python, no dependencies, no compiled parts. Paths go
through os.path, CLAUDE_CONFIG_DIR is honoured everywhere, and the installer
picks an interpreter name that works through your shell rather than hardcoding
one. The only platform-specific code is forcing UTF-8 on stdout, which Windows
needs and which is harmless elsewhere.
Verify the whole thing on your own machine:
python tests/test_session_weight.py # arithmetic, the gate, the two traps
python tests/test_compat.py # syntax floor, entry points, hook outputtest_compat.py finds every other Python installed on your machine and re-runs the
suite under each, so a version difference shows up as a failure rather than a
surprise later.
Seven signals are measured. Context has the only vote on whether to move; the rest only sharpen how urgent it is.
| signal | threshold |
|---|---|
| context vs the wall | ≥ 85% → hand off, ≥ 95% → stop asking and act |
| assistant turns | ≥ 900 |
| tool calls | ≥ 600 |
| active working time | ≥ 4 h |
| auto-compaction already fired | any |
Nothing is offered below 62% of the wall, whatever else tripped. This gate exists because the other signals are proxies for context pressure invented for a world where context could not be measured directly. Measured against the session that built this: 4.3 h of work plus two earlier compactions scored "hand off now" while context sat at 147,527 of 977,000 — 15%. Moving then would have thrown away 829,473 tokens to save nothing.
Two measurement details that matter more than they look:
- Active time is the sum of gaps under 10 minutes, never last-minus-first. A session left open overnight reads as 44 h of span and 11 h of work; scoring the span fires a handoff on an idle session.
- Compactions are counted from the typed transcript row, never by searching for a marker string. Search for the marker once and it appears in your own tool output, and the count inflates itself.
The prompt appears at most once per tier — 200 more turns, or another tenth of the wall — with a 15-minute floor. It never fires inside a subagent.
measure → ask → export → create the continuation → it reads the parent
The export writes five files: every user message verbatim (including the ones sent mid-turn, which are easy to lose), every substantial assistant message, the full transcript with tool payloads clipped, an index of counts, and whatever earlier compactions discarded.
The continuation is then created and woken headlessly to read the export before you ever open it. It must reply with counts that match the index — if they do not match, the read was partial and the handoff is not done. That read happens in a session nobody is waiting on, so the expensive part of a migration costs you no wall-clock time.
Afterwards you get the id and the name:
claude --resume 7157caa1-11ce-4f29-a46a-09913d483fb0
or search /resume for the name, which carries the parent's topic words plus
(cont. 2).
Every claim above is checkable on your own machine. The scripts print numbers, not reassurance:
# where does my session actually end, and why?
session_weight.py --explain
# what is the current weight, with every signal named?
session_weight.py --session-id <uuid>
# machine-readable
session_weight.py --session-id <uuid> --jsonTo confirm a config change took effect, do not trust the file — read the transcript. Find the first turn whose token total passes the old trigger and check that no new compaction row follows it.
Stated plainly, because a tool that measures things should be honest about what it has not measured:
- The reserves (~20k / ~13k / ~3k) are derived from observed behaviour. A future release could change them — see Compatibility for the three layers of evidence that protect against that, and note that layer 3 is the guess.
- The send-refusal wall has been computed and corroborated, not deliberately hit. The guard is designed so you never reach it.
- Tested on Windows with Python 3.11, 3.12 and 3.14, and syntax-checked against the 3.8 grammar. Linux and macOS should be fine — nothing platform-specific remains beyond console encoding — but neither has been run end to end.
- The five-file export and the scorer are exercised by the test suite. The headless
wake turn is the part that depends on your
claudebinary being launchable; if it is not, the export still succeeds and the tool tells you what to do instead. - Prompt caching: a handoff starts a new session, so its cache starts cold. For a session near the wall that is a good trade; it is still a cost.
Bug reports welcome, especially "the numbers were wrong on my setup" — include
the output of --explain. If a Claude Code release moves the arithmetic, that is
the report that fixes it fastest.
Before opening a PR, run both suites:
python tests/test_session_weight.py
python tests/test_compat.pySECURITY.md documents exactly what this reads, what it writes, and what it
sends over the network (nothing). Worth a look before installing anything that
touches your session files.
MIT — see LICENSE. Free to use, modify, and redistribute, including commercially. The one condition is that the copyright notice and licence text travel with it, so a fork or a repackaged copy still says where it came from.
If you use the approach or the findings — particularly the clamped-window
diagnosis — a link back is appreciated. CITATION.cff is there so GitHub's
"Cite this repository" button produces something correct.
Authored by IRDkiya.