Re-inject the working state Claude Code loses when it compacts context.
When Claude Code compacts a long session it swaps the transcript for a short summary, and that summary is lossy: the file you were mid-edit on, the command that just failed, the approach you ruled out, the constraint you set 40 turns ago — gone.
unforget reads the on-disk transcript (append-only, survives compaction), works out exactly what
was dropped, and re-injects a compact digest of it through a SessionStart hook — so the fresh
context picks up where you left off.
- Zero runtime dependencies. Local only, never a network call.
- Reads only
~/.claude(or$CLAUDE_CONFIG_DIR); writes nothing but its own hook config. - Silent when there's nothing to recover — safe to wire to a hook.
npm install -g unforget # or: bunx unforget / npx unforgetAdd the hook once; every post-compaction session then starts with the digest re-injected:
unforget init # shows the settings.json change, asks, backs up, then writes
unforget init --remove # uninstall
unforget init --print # print the hooks block instead of writingIt registers a SessionStart hook (matcher compact) with absolute paths, so it still fires when
Claude Code launches from a GUI app with a minimal PATH. Symlink-safe; refuses a settings file it
can't parse.
Inspect or verify by hand:
unforget digest [path] # what the newest compaction dropped (--boundary N, --json)
unforget doctor # hook installed? paths on disk? last injection fresh?The built-in summary already covers most mechanical rediscovery; unforget catches what it drops. In the author's own dogfooding — 135 compaction boundaries, 62 of them post-install, self-measured:
- 98% of the working state the summary dropped was carried by the digest (22.7% of all rediscovery events, most of which the summary already covered).
- 3.15 → 1.97 average rediscoveries per compaction, pre- vs post-install (files re-read / commands re-run that were already known). The pre-install half is correlational — different sessions, not a controlled A/B.
- ~854 tokens of that state re-injected per compaction that needed it.
Single-machine sample — directional, not a guarantee. Run bun bench/retro.ts against your
own ~/.claude to measure it yourself.
- Find the newest transcript's last
compact_boundary; read which message UUIDs survived. - Dropped set = messages since the previous boundary that weren't preserved.
- Extract a deterministic digest from the dropped records: active task, in-flight edits, failed
commands + last test result, dead-ends, constraints (
don't / always …), next step. - Drop anything the built-in summary already carries (summary read as a filter, never a source).
- Render markdown, empty sections omitted, capped at 9,500 characters.
Claude Code flushes the new boundary to disk only after hooks run, so the live hook (inject)
windows from the last flushed boundary to end-of-file — the just-compacted turns — while digest
reconstructs it after the fact. See docs/hook-contract.md.
Reconstructing working state means reading message content — but only under your own ~/.claude,
writing nothing outside its own config, with no network calls. Nothing leaves your machine.
bun test && bunx tsc --noEmit && biome check src test && bun run buildMIT
