Skip to content

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·D once and earns 0.1·D back on every later turn.

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

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

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

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

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

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.

Illustration of when a context cut pays for itself. Bars measure the further turns a cut must survive, against the README's worked half cut of 18. Removing a tenth of the suffix needs 170 turns and removing a quarter needs 56, both past it; removing a half needs exactly 18 and removing two thirds needs 9. The same half cut taken immediately before a resume needs none, because the resume was going to rewrite that suffix anyway. The intake filter has no break-even at all. Illustration, on an invented 120,000-token suffix. The arithmetic is the README's: a cache read bills at 0.1× and the one-hour write it measured over 26,194 turns bills at 2.0×, so an edit pays 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.

Bar chart of what a long Claude Code session is made of, measured across 563 transcripts and 175.6 MB of message content. Tool result and tool use inputs are 91.6% of the bytes; everything else is 8.4%. A count of what a session is made of, and nothing more: bytes on disk say nothing on their own about what removing any of them would cost or save. That is the rest of this page.

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.

Illustration of where the filter acts in a turn. Five requests go out: two carry a tool result in full, placed past the last cache_control breakpoint, and three carry a pointer instead, each marked with the winnow rule that fired — C1, C3 and B2. A meter then splits one request into the cached prefix, read at a tenth of the input rate, and the uncached tail the full result sits in. A pruner instead edits the prefix and pays 1.9 S minus 2 D once; that half is hatched, because winnow fork writes it but no fork has been resume-tested. Illustration, on invented request numbers and proportions. Only the three rules needing no hindsight can fire on the wire — C1, C3 and B2. C2, B1 and A1 all need to see the conversation's future, and a policy that did would change the prefix under the cache. The hatched half is software you can run — 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 savings
Illustration of one API response written to disk as five separate records, all carrying the same requestId and the same message usage, gathered by a bracket into the one turn they are counted as. Summing the records instead inflates T by 1.7 to 2.4 times. Illustration; five is the drawing's number. Claude Code writes one record per content-block group of a response, so the same usage appears several times over — and the 1.7 to 2.4× is what summing them cost on that install's transcripts, measured against its own request count.

05 · 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.

The winnow mark drawn large: a wide solid intake, walls converging over nine rows, down to a solid spout, because the part that narrows a session is now written. 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.