Sick of waiting for "Compacting conversation..."? This hook does it in the background while you work — you never see the spinner.
Heads up: This was vibecoded in an afternoon. It's working well for me, but read the code before you run it on your sessions.
Pro tip:
tail -f ~/.claude/hooks/streaming-compactor/compactor.logto watch it work in real time.
Jump to install — clone, install, done.
A Claude Code hook that automatically compacts conversation context to prevent context window exhaustion. It uses Claude Code's native /compact (same quality as manual compaction) but runs it asynchronously on a clone — so you keep chatting while compaction happens in the background.
After the swap, you need to manually resume the session. Claude Code doesn't currently have hooks for auto-resume, so the compactor shows a system message with the /resume command to copy-paste. Hopefully the Claude Code team will add resume hooks eventually.
How it works
The compactor uses a two-threshold hysteresis approach:
0% 50% 70% 100%
|──────────────|──────────────────|─────────────────|
^ ^
│ │
Clone JSONL & Merge compacted
run /compact clone + new turns,
in background swap the file
(you keep chatting) (exit + resume)
-
Check — On every Claude Code
Stopevent, the hook readsmessage.usagefields from the session's JSONL transcript to compute context window utilization. This is free — no additional API calls. -
Compact (at 50%) — When utilization crosses the compact threshold, the JSONL is copied to a clone file. A background process runs
claude --resume <clone_id>with/compactpiped to stdin, triggering Claude Code's native compaction on the clone. You keep chatting normally — the active session is never touched during this step. -
Swap (at 70%) — When utilization crosses the swap threshold and the clone is ready, the original JSONL is backed up and replaced with:
[compact_boundary + summary from clone]+[turns that happened since the copy]. Any turns you made while compaction was running are preserved as a tail after the summary. -
Resume — After a swap, a system message tells you to exit and run
claude --resume <session_id>. You need to copy-paste this command — there's no hook to auto-resume yet.
Recursion prevention
When /compact runs on the clone, it triggers Stop hooks. The compactor tracks clone session IDs in state/clones/ and silently skips any session that's a known clone.
Prerequisites
- Claude Code CLI installed and on your PATH
- Python 3.10+ (stdlib only — no pip dependencies)
Install
git clone https://github.com/enigma/claude-streaming-compactor.git
cd claude-streaming-compactor
bash install.shThe installer:
- Copies compactor files to
~/.claude/hooks/streaming-compactor/ - Creates
state/androtated/subdirectories - Adds the compactor hook to
~/.claude/settings.json(backs up first) - Is idempotent — safe to run multiple times
Alternative: use uv instead of python3
If you prefer uv, edit ~/.claude/hooks/streaming-compactor/on_stop.sh and replace:
echo "$INPUT" | python3 "$COMPACTOR_DIR/compactor.py" check
with:
echo "$INPUT" | uv run "$COMPACTOR_DIR/compactor.py" check
Also update the background worker call in compactor.py (search for ["python3", and replace with ["uv", "run",).
Uninstall
The uninstaller:
- Removes the compactor hook entry from
~/.claude/settings.json(backs up first) - Prompts whether to delete
~/.claude/hooks/streaming-compactor/(warns about backups)
Manual removal
- Edit
~/.claude/settings.json— remove the hook entry that referencesstreaming-compactor/on_stop.shfrom the"Stop"array - Delete the directory:
rm -rf ~/.claude/hooks/streaming-compactor
Restore a backed-up session
If a session was already swapped and you want the original back:
# List backups ls -la ~/.claude/hooks/streaming-compactor/rotated/ # Backups are named: <session_id>.<unix_timestamp>.jsonl # Copy the one you want back to its project directory: cp ~/.claude/hooks/streaming-compactor/rotated/<session_id>.<timestamp>.jsonl \ ~/.claude/projects/<project-slug>/<session_id>.jsonl
The project slug is the project path with / replaced by -, e.g.:
/Users/you/my-project → -Users-you-my-project
Configuration
Edit ~/.claude/hooks/streaming-compactor/config.json:
{
"compact_threshold": 50,
"swap_threshold": 70,
"context_window_size": 200000
}| Key | Default | Description |
|---|---|---|
compact_threshold |
50 |
Context usage % to start background compaction (clone + /compact) |
swap_threshold |
70 |
Context usage % to swap in the compacted version |
context_window_size |
200000 |
Token capacity of the model (Claude Opus 4 = 200k) |
Dry run
To see what the compactor would do without actually acting:
echo '{"session_id":"test","transcript_path":"/path/to/session.jsonl","cwd":"/tmp"}' | \ python3 ~/.claude/hooks/streaming-compactor/compactor.py check --dry-run
Check the log for output:
tail -20 ~/.claude/hooks/streaming-compactor/compactor.logHow context usage is measured
The compactor reads the last message.usage object from the session's JSONL transcript. This reflects what the Claude API actually reported for that turn — the real token count sent to the model.
Key details:
- Before swap: The user's live session doesn't know about the clone or its
compact_boundary. Usage keeps climbing naturally in the original file. The compactor measures this real, growing pressure — which is exactly what triggers the thresholds. - After swap: The swapped file contains only
[boundary + summary + tail]. On resume, Claude Code loads content from the boundary onwards, and the next API response reports the reduced token count. Usage drops. - Existing boundaries: If a session already has
compact_boundarymarkers (from previous manual/compactor Claude Code's own compaction), Claude Code only loads content after the last boundary. The API-reported usage already reflects this — the compactor doesn't need to account for boundaries itself. - Cache tokens count: The formula is
(input_tokens + cache_creation_input_tokens + cache_read_input_tokens + output_tokens) / context_window_size. Cached tokens still occupy context window space.
Known limitations
- Manual resume required: After a swap, you must copy-paste the
/resume <session_id>orclaude --resume <session_id>command shown in the system message. Claude Code doesn't have hooks for auto-resume (yet). - JSONL format dependency: The
parentUuidchain rewriting andsessionIdpatching assume Claude Code's current JSONL transcript format. If the format changes, the swap step may produce sessions that can't be loaded. Backups inrotated/allow recovery. - One API call per compaction: The native
/compacton the clone uses one API call. This is the only non-free operation. - Clone cleanup: The clone JSONL is created in the same project directory as the original. It's cleaned up after swap or on failure, but if the compactor crashes mid-run, orphan clone files may remain. They're safe to delete (check
state/clones/for active clones first). - macOS/Linux only: Uses
fcntlfor file locking (not available on Windows).
Troubleshooting
Watch the log live
Follow the compactor in real time — it logs every check, compaction, and swap:
tail -f ~/.claude/hooks/streaming-compactor/compactor.logYou'll see lines like:
[2026-02-18T16:29:54.483210Z] check: [209f1ac3] pct=31.4% state=none
[2026-02-18T16:30:35.775461Z] check: [175726f2] pct=52.3% state=none
[2026-02-18T16:30:35.775761Z] check: [175726f2] crossed compact threshold (50%). starting compaction.
[2026-02-18T16:30:36.012345Z] compact: [175726f2] cloned to a1b2c3d4 (847 lines)
[2026-02-18T16:30:36.234567Z] compact: [175726f2] starting native /compact on clone a1b2c3d4...
[2026-02-18T16:31:45.123456Z] compact: [175726f2] native compaction complete on clone a1b2c3d4
[2026-02-18T16:33:12.654321Z] check: [175726f2] crossed swap threshold (70%). swapping.
[2026-02-18T16:33:12.987654Z] swap: [175726f2] done. boundary=2 + tail=42 = 44 lines.
Check recent log entries
tail -50 ~/.claude/hooks/streaming-compactor/compactor.logView current state
cat ~/.claude/hooks/streaming-compactor/state/*.json 2>/dev/null
Clear stuck state
If compaction is stuck (e.g., background worker crashed):
rm -f ~/.claude/hooks/streaming-compactor/state/*.json rm -f ~/.claude/hooks/streaming-compactor/state/clones/*
This is safe — it just resets the compactor. Any leftover clone JSONL files in your project directory can be deleted manually.
Session won't load after swap
- Check for a backup:
ls ~/.claude/hooks/streaming-compactor/rotated/ - Restore it to the project directory (see "Restore a backed-up session" above)
- Clear the state files to prevent re-swapping
License
MIT