Input-side token compression for Claude Code. Where rolling-context manages tokens already in your context, lean-context stops waste before it enters — and the two cooperate when both are installed. Everything is lossless-or-recoverable: the model either sees the real bytes, or an honest pointer/diff/tee to bytes it can get back with one more call. Every transform fails open.
An identical re-read of an unchanged file (same path/offset/limit, same content, same agent, within 20 min) is replaced by an orienting note (age, size, digest, outline) pointing at your earlier read. Escape valve: the next identical Read returns full content.
The big one. When you read a file you read before and it changed, lean-context sends only the changed hunks as a unified diff with real line numbers — everything else is "unchanged from what you already have." Read → edit → read-to-verify loops, which normally resend the whole file, now resend ~the diff. Only fires when your earlier read was served in full and is provably still in context (guarded by compaction + rolling-context signals); wholesale rewrites fall back to a full read; escape valve always applies.
The same engine that maps code maps markdown, HTML/Vue/Astro/SVG and CSS/SCSS: headings, landmark tags and selectors are kept with real line numbers, the prose or declarations between them are elided with counts. Measured on a real corpus, non-code files are a quarter of all first-read bytes and markdown is the single largest non-code extension, so this is not a side case.
Intent scales the threshold rather than switching mapping off. Treating "error-hunting" as a veto looked safe but cost most of the opportunity, because that vocabulary ("error", "failed", "debug") shows up in ordinary narration far more often than real debugging does. While the agent looks like it is investigating, only files past a much larger floor are mapped — where a full read would dominate the context anyway — and the escape valve still returns full content on the next identical Read.
codemap maps an entire directory in one call — folder tree, file-type counts, every code file's signatures with real line numbers, bodies elided. Measured 84% char reduction on a real 51-file repo. For orientation; the skill tells the model to Read real files before editing. Also fires automatically on large single-file code reads while exploring (never while error-hunting).
Three tiers, best first:
- rtk (the real binary). rtk is a maintained Rust tool with ~50 per-command output filters. lean-context uses it the safe way:
rtk pipe --filter <name>reshapes the already-captured output — your command runs completely untouched, we only shrink what enters context. Covers pytest, vitest, tsc, git log/diff/status, cargo/go test, mypy, ruff, prettier, grep, and more. Measured: a 300-test pytest run 8,679 → 322 chars (96%). rtk is lazy-downloaded (checksum-verified) on first session only if you opt in withLEAN_CONTEXT_RTK_DOWNLOAD=1, or pointLEAN_CONTEXT_RTKat an existing binary. - Built-in routes (no binary needed):
dotnet build, test runners, andnpm/pnpm/yarn/pip install— keep errors/warnings/summary, drop the spam, dedupe repeated diagnostics. - Generic collapse: any command's runs of identical consecutive lines collapse with explicit counts.
All command compression is backed by a tee file: the full output is written to disk and its path is in the header, so nothing aggressive is ever unrecoverable — one Read gets it all back.
Opt-in max coverage: set LEAN_CONTEXT_RTK_REWRITE=1 to also rewrite supported simple commands to their rtk equivalent before they run (git status → rtk git status), covering rtk's full ~50-command set born-compressed. Off by default because it changes the command that actually executes.
Identical matches within a file collapse with counts; per-file caps. Skipped entirely for error-hunting patterns (every occurrence may matter).
- PowerShell gets everything Bash gets. It was previously absent from the hook matcher, so on Windows — where most commands go through that tool — a large share of shell output was never even seen.
- WebFetch returns a page rendered to markdown, so it gets the markdown outline treatment; WebSearch gets repetition collapse. Both tee-backed.
- Glob path lists fold by directory: the shared stem is written once and every filename listed under it, so each path is still recoverable by concatenation.
- Subagent reports and task output get repetition collapse only. A subagent's final report is often the only record of work the main agent never saw, so nothing there is restructured or summarized.
MCP servers routinely return large JSON or HTML. lean-context shrinks it deterministically and losslessly-or-recoverably: minify pretty-printed JSON (whole-output or inside ```json fences — still valid JSON, exact values preserved), drop <script>/`<style>`/comment blocks the model never needs, and collapse runs of identical lines. Tee-backed, so the full untouched output is one Read away. Structured data is never paraphrased. Measured live: a script/style-heavy HTML payload from a real MCP tool, 22,711 → ~750 chars (97%). Mixed outputs containing images are left untouched.
- Per-agent scoping — state keyed by
transcript_path; simultaneous subagents never share dedup/diff knowledge. - Compaction — a
PreCompacthook clears "already seen" knowledge and stored diff bases before compaction runs;SessionEndtoo. - rolling-context — polls the proxy's
/lean/status(v1.9.0+); any read recorded before the last compression injection is never turned into a reference or diff base — full content is served instead.
/plugin marketplace add https://github.com/NodeNestor/nestor-plugins
/plugin install lean-context
Requires Python 3.7+ (stdlib only). To enable the real-rtk tier, either install rtk yourself and it's auto-detected, or set LEAN_CONTEXT_RTK_DOWNLOAD=1 to let the SessionStart hook fetch the checksum-verified binary into the plugin's data dir.
/lean-context:gain
/lean-context:off # this conversation only
/lean-context:on # ...and back
/lean-context:off --global # every session on this machine
/lean-context:status # what is in effect, and why
No restart, no config edit. Everything passes through untouched while off, and
/lean-context:gain says so rather than just reporting zeros. This is the switch
to reach for when a tool result looks wrong and you want to rule lean-context out
in one command — which matters more than the savings do.
Per-session is the default scope because one window doing careful review wants raw tool output while the others still want the savings. The toggle is keyed on session id, not transcript, so subagents inherit the setting of the session that spawned them.
/lean-context:default off # new sessions start uncompressed
/lean-context:on # ...turn it on where you want it
Useful if you would rather reach for compression deliberately than have it always on. Sessions that already toggled themselves keep their own setting.
Precedence, most specific first:
| Scope | Set by | Beats |
|---|---|---|
LEAN_CONTEXT_DISABLE=1 |
settings.json env | everything |
| machine-wide | --global |
session and default |
| this session | /lean-context:on|off |
the default |
| default | /lean-context:default |
— |
The env var and the global flag deliberately win over a session opting in, so
there is always one switch that stops everything. LEAN_CONTEXT_DISABLE=1 needs
a Claude Code restart, so it suits permanent opt-out rather than a mid-session
check.
/lean-context:config show every setting and where it came from
/lean-context:config preset aggressive floors near zero — more coverage
/lean-context:config preset conservative higher floors — fewer, larger interventions
/lean-context:config bash_min_chars 500 change one setting
/lean-context:config --project ... write ./.lean-context.json instead
/lean-context:config --reset clear it
Resolution order, lowest priority first: defaults → preset → ~/.lean-context/config.json
→ ./.lean-context.json → LEAN_CONTEXT_* environment. The config printout names
the source of every value, so a setting that "won't change" is never a mystery.
No restart — changes apply to the next tool call.
On the floors. They are a cheap pre-filter, not the safety mechanism. The
hook process runs on every matched tool call whether or not a floor is met, so a
floor skips a few microseconds of in-process work rather than a process spawn.
min_saving_ratio is what actually protects you: any transform failing to save
that fraction is discarded and the original passes through untouched. Lowering
floors therefore buys coverage very cheaply. Swept on a read-heavy sample:
| structural-map floor | saved | Read transforms fired |
|---|---|---|
| 12k (old default) | 27.8% | 29 |
| 6k (default) | 32.8% | 46 |
| 3k | 34.3% | 60 |
| 3k + every other floor at 500 | 34.7% | 61 |
6k is the knee — most of the value, without mapping files small enough that the
map is barely smaller than the file. Removing the floors entirely buys 0.4
points on top, which is why aggressive exists but is not the default.
| Env var | Default | Meaning |
|---|---|---|
LEAN_CONTEXT_PRESET |
balanced |
conservative | balanced | aggressive |
LEAN_CONTEXT_MIN_SAVING_RATIO |
0.20 |
the real guard: discard transforms saving less |
LEAN_CONTEXT_DISABLE |
— | 1 disables everything |
LEAN_CONTEXT_DIFF |
1 |
0 disables differential reads |
LEAN_CONTEXT_CODEMAP |
1 |
0 disables auto single-file codemap |
LEAN_CONTEXT_RTK_PIPE |
1 |
0 disables the rtk pipe tier |
LEAN_CONTEXT_RTK_DOWNLOAD |
— | 1 lets SessionStart download rtk |
LEAN_CONTEXT_RTK_REWRITE |
— | 1 enables PreToolUse command rewriting (re-executes via rtk) |
LEAN_CONTEXT_RTK |
— | explicit path to an rtk binary |
LEAN_CONTEXT_BASH_ROUTES |
1 |
0 disables built-in command routes |
LEAN_CONTEXT_MCP |
1 |
0 disables MCP output compression |
LEAN_CONTEXT_WEB |
1 |
0 disables WebFetch/WebSearch compression |
LEAN_CONTEXT_DEDUP_WINDOW |
1200 |
seconds a read stays dedup/diff-able |
LEAN_CONTEXT_GREP_PER_FILE_CAP |
25 |
max grep matches kept per file |
LEAN_CONTEXT_BASH_MIN_CHARS |
1500 |
shell output floor |
LEAN_CONTEXT_GREP_MIN_CHARS |
2000 |
grep output floor |
LEAN_CONTEXT_MCP_MIN_CHARS |
3000 |
MCP output floor |
LEAN_CONTEXT_WEB_MIN_CHARS |
3000 |
WebFetch/WebSearch floor |
LEAN_CONTEXT_GLOB_MIN_CHARS |
2000 |
Glob path-list floor |
LEAN_CONTEXT_REPORT_MIN_CHARS |
4000 |
subagent report floor |
LEAN_CONTEXT_CODEMAP_MIN_CHARS |
12000 |
map floor while exploring |
LEAN_CONTEXT_CODEMAP_DEBUG_MIN_CHARS |
40000 |
map floor while error-hunting |
Every floor is a cheap pre-filter, not the safety mechanism. MIN_SAVING_RATIO
is: any transform that fails to save 20% is thrown away and the original passes
through, so lowering a floor costs a little work, never correctness.
Hooks: PostToolUse (Read/Grep/Bash/PowerShell/WebFetch/WebSearch/Glob/Agent/TaskOutput/MCP transforms), PreToolUse (opt-in rtk rewrite), PreCompact/SessionEnd (invalidation), SessionStart (opt-in rtk bootstrap). Replacements are rebuilt in each tool's original output shape (Claude Code validates updatedToolOutput against the tool's schema). State is per-agent JSON + content blobs under ${CLAUDE_PLUGIN_DATA}, pruned after 48h. Pure-stdlib Python; the only optional external piece is the rtk binary, which is never required.
python test/test_dispatch.py
Covers dedup + escape valve, differential reads (small change → diff, wholesale → full), codemap intent gating, all three command tiers (mock rtk, built-in routes, collapse), rtk PreToolUse rewrite, grep, PreCompact/SessionEnd + rolling-context invalidation, shape preservation, and every disable switch.
MIT (the rtk binary is separately licensed by its authors, Apache-2.0, and downloaded only on opt-in).