GitHub - enigma/claude-streaming-compactor

6 min read Original article ↗

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.log to 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)
  1. Check — On every Claude Code Stop event, the hook reads message.usage fields from the session's JSONL transcript to compute context window utilization. This is free — no additional API calls.

  2. 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 /compact piped to stdin, triggering Claude Code's native compaction on the clone. You keep chatting normally — the active session is never touched during this step.

  3. 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.

  4. 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.sh

The installer:

  • Copies compactor files to ~/.claude/hooks/streaming-compactor/
  • Creates state/ and rotated/ 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

  1. Edit ~/.claude/settings.json — remove the hook entry that references streaming-compactor/on_stop.sh from the "Stop" array
  2. 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.log

How 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_boundary markers (from previous manual /compact or 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> or claude --resume <session_id> command shown in the system message. Claude Code doesn't have hooks for auto-resume (yet).
  • JSONL format dependency: The parentUuid chain rewriting and sessionId patching assume Claude Code's current JSONL transcript format. If the format changes, the swap step may produce sessions that can't be loaded. Backups in rotated/ allow recovery.
  • One API call per compaction: The native /compact on 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 fcntl for 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.log

You'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.log

View 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

  1. Check for a backup: ls ~/.claude/hooks/streaming-compactor/rotated/
  2. Restore it to the project directory (see "Restore a backed-up session" above)
  3. Clear the state files to prevent re-swapping

License

MIT