stdlib only no network nothing installed
winnow
An instrument for deciding whether pruning a Claude Code session is worth what it costs.
A long Claude Code session is mostly tool output — across 563 transcripts and 175.6 MB of message content, tool_result and tool_use inputs are 91.6% of the bytes. Several tools will strip that for you. None of them can tell you whether stripping it saved you anything.
The pruner is written and not yet proved. winnow context, winnow filter, winnow savings, winnow plan, winnow fork and winnow recover all run, alongside the harness that makes the inherited tool survivable under an unattended session — and three of milestone 2's acceptance criteria are still unanswered.
No claim that pruning a Claude Code session saves money has been made here, or anywhere else.
what is here
Six things, and the last one is what nobody has measured
Removing context does not obviously save money
Cache reads bill at 0.1× and writes at 1.25× or 2×, and matching is exact and prefix-ordered — so an edit invalidates everything after the cut. A prune pays
1.9·S − 2·Donce and earns0.1·Dback on every later turn.Measure a session before touching it
winnow contextreports what is in one session's context window and where it came from: the total read exactly out of the last priced request, the parts apportioned inside it. It writes nothing.Never let the bytes into the cache
winnow filteris a local pass-through proxy. A tool result a rule would strip goes out in full on the one request the model acts on it, placed after the lastcache_controlbreakpoint so the API never writes it to cache, and is dropped on the next request.Price what the filter actually did
winnow savingsreads~/.winnow/filter.jsonl, joins each line to the Claude Code transcript onrequest_idto recover the session and how many API turns followed, and prices it. The simulation is a corpus average; this is one install's ledger.Run the inherited tool without it killing the session
The vendored tool assumes an interactive user: it can defer a prune until you quit, ask you to run
init, and start a daemon that willSIGKILLa session it judges too large. Under an unattended harness the session that daemon would kill is the one the tool is running inside.What has not been answered
There is no
winnow bench.winnow forkandwinnow recoverare written, but three of milestone 2's acceptance criteria have not been answered, so the milestone is built and not passed.
01 · arithmetic
Removing context does not obviously save money
Cache reads bill at 0.1× and writes at 1.25× or 2×, and matching is exact and prefix-ordered — so an edit invalidates everything after the cut. A prune pays 1.9·S − 2·D once and earns 0.1·D back on every later turn.
Break-even is T* = 19·(S/D) − 20 further turns. The ratio decides, not the size of the session: cut half the suffix and it pays for itself in 18 turns; cut a tenth and it needs 170 more turns than the session has had.
T* = 19·(S/D) − 20
further turns, before the cut has paid for itself.
The 2.0× is not the list-price assumption. It is a measurement over 26,194 turns of one install where every main-thread turn wrote at the one-hour class.
1.9·S − 2·D once and earns 0.1·D a turn back. S/D decides — the same four bars are drawn for a session ten times the size.02 · context
Measure a session before touching it
winnow context reports what is in one session's context window and where it came from: the total read exactly out of the last priced request, the parts apportioned inside it. It writes nothing.
It replaces winnow inspect, which still runs and now opens by deprecating itself: every token figure inspect prints is a lifetime sum rather than a window, and its cache_read_input_tokens reads 18,378,780 on a session whose window was 219,485. Milestone 1's number is still inspect's — tier CB strips 10.2% of message content pooled and 8.8% at the median, against the 22.6% / 21.6% section 6 of docs/SPEC.md recorded. It misses, and it was built to be allowed to say so.
python -m winnow context <session-id>About 500 lines with 30 tests. Every figure it prints says whether it was read, derived or estimated, and --audit prints the constant that would zero the residual beside the fact that it was not applied.
03 · filter
Never let the bytes into the cache
winnow filter is a local pass-through proxy. A tool result a rule would strip goes out in full on the one request the model acts on it, placed after the last cache_control breakpoint so the API never writes it to cache, and is dropped on the next request.
There is no break-even term, because nothing is edited. On a replay over 175 historical sessions it reaches 8.21% and is worth +3.76% of the bill, against the tier-CB pruner's 10.17% and +3.27% — 1.1×, and positive in 175 sessions out of 175 rather than 97 of 168. That replay was computed at a 2,048-byte floor the filter has since left behind.
winnow fork writes it — but the 100-fork resume test has never produced a result.04 · savings
Price what the filter actually did
winnow savings reads ~/.winnow/filter.jsonl, joins each line to the Claude Code transcript on request_id to recover the session and how many API turns followed, and prices it. The simulation is a corpus average; this is one install's ledger.
The filter is stateless, so it re-drops the same result on every later request that still carries it: a ledger of 1,283 removal events on one install holds 49 distinct results, and summing bytes_dropped over lines would report 27× what was removed. The figure is modelled, not billed, and the command says so in its own output.
python -m winnow savings05 · safe
Run the inherited tool without it killing the session
The vendored tool assumes an interactive user: it can defer a prune until you quit, ask you to run init, and start a daemon that will SIGKILL a session it judges too large. Under an unattended harness the session that daemon would kill is the one the tool is running inside.
Orchestrator-safe mode is one switch and six guarantees, each held from outside the tree it wraps: no termination, no resume, no updater, no writes to ~/.claude, no competing with the harness's own context and cost controls, and nothing written into the model's memory.
export WINNOW_ORCHESTRATOR=1python -m winnow safe check
safe check prints what would be refused and why. The mode has never run inside a real orchestrated cycle: everything was exercised by hand in a container, and no network call was proved absent, only switched off.
06 · unbuilt
What has not been answered
There is no winnow bench. winnow fork and winnow recover are written, but three of milestone 2's acceptance criteria have not been answered, so the milestone is built and not passed.
No claim that pruning a Claude Code session saves money has been made, by anyone. The 100-fork resume test was attempted on 2026-09-07 and blocked by its environment before a fork was written; nothing has been labelled against the 200-sample bar; the week of disk cost has no observations and no figure for it appears anywhere in the repository. The kill criteria are written against those three rows, and stopping on one of them is the intended outcome rather than a failure of it.
context, filter and savings are the intake. The spout is solid because winnow fork writes now — what is missing is no longer code, it is the three measurements that would say whether running it is safe.plan, fork and recover are built, and every criterion a test suite can settle is met: G5 pairing with --force unable to reach it, the cold-age refusal, no net inflation, byte-identical output across two runs, and the fork-to-recover round trip digest-checked on every pointer. The three that are left need production transcripts, a person reading turns, and a week — none of them is short of code.
start here
Clone it and read one session
git clone https://github.com/Xapicc/winnowcd winnowpython -m winnow context <session-id>
winnow publishes to no package channel, so installing means a checkout. context writes nothing: it reads one session and prints the window, broken down by where each part of it came from.
export WINNOW_FILTER=1python -m winnow filter --ledger ~/.winnow/filter.jsonlexport ANTHROPIC_BASE_URL=http://127.0.0.1:8789
The filter refuses to start without WINNOW_FILTER=1, because running it puts a process of your own in front of your own key. It relays your auth headers upstream, holds none of its own and logs none — and it forwards the original bytes unchanged on any failure to parse or rewrite.
Killing the process is not the off switch. ANTHROPIC_BASE_URL is fixed in a client's environment when it starts, so a listener that goes away takes every request with it. On a running install, touch ~/.winnow/filter-off and the next request is relayed untouched.