the intake filter
Never let the bytes into the cache
The one position that pays nothing.
A pruner edits a conversation that is already cached, so it pays 1.9·S − 2·D once and has to earn it back. winnow filter is a local pass-through proxy that keeps the bytes out of the cached prefix in the first place: a tool result a rule would strip is sent 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.
It runs. winnow filter is src/winnow/filter.py and proxy.py — stdlib only, about 1,350 lines with 804 tests, of which a property test over generated conversations asserts the cache-write invariant and a golden pins the emitted bytes.
position
Where it acts in a turn
Position is the whole mechanism. The full send sits past the last cache_control breakpoint, so the API never writes it — and what is never written is never read back at 0.1× on any later turn either.
Nothing in the cached prefix is edited, so nothing is invalidated. That is the difference the arithmetic turns on, and it is a difference of position rather than of policy: the rules are the same rules.
winnow fork writes it — but the 100-fork resume test has never produced a result.rules
Three of the six can fire on the wire
The filter reaches less than the pruner, and the reason is structural rather than a matter of tuning: a rule that has to see the conversation's future cannot run on a request that is going out now.
- C1 locator
GlobandLS, andGrepwhereoutput_modeisfiles_with_matchesorcount. The output is a list of paths whose only consumer is the call that followed it.- C3 passing verification
Bashmatching the verification pattern —npm test,pytest,go test,cargo test,tsc,ruff,mypyand their neighbours — and only whereis_erroris false. A failing verification is never stripped: the failure is the information.- B2 Bash inspection
Bashwhere the first token of the first segment before&&or|matches the inspection pattern, so a pipeline headed bylsorgit statuscounts and one headed bypythondoes not, whatever follows.
And three of them cannot
Each of these decides what to strip by looking at something that has not happened yet. A policy that did would also change the prefix under the cache, which is the cost the filter exists to avoid.
- C2 exact duplicate
- Strips the earlier of two byte-identical results, which means knowing a later one arrives.
- B1 superseded read
- Strips a
Readbecause a laterReadcovers it. The later one has not happened yet. - A1 read then written
- Strips a
Readbecause a laterEditorWritemade it stale. Opt-in even in the pruner, and the ruledocs/SPEC.mdsection 4 itself calls the most likely to be wrong.
Section 4 of docs/SPEC.md sets five universal guards, applied before any rule at any tier: the last results in the session are kept, a result under --min-bytes is kept, is_error: true is never stripped, a pointer longer than the content it replaces is not written, and every tool_use in the output keeps its matching tool_result. The filter's own floor is 256 bytes rather than the pruner's 2,048, because it sends a candidate once in full and the pointer then lives in the cached prefix.
the cost model
What it avoids, term by term
Per result of D tokens over T following turns, the baseline is a 2.0× cache write plus a 0.1× read on every later turn. The filter pays 1.0× once and nothing after.
There is no break-even term. It is cheaper from the first request, at every S/D, which is the one thing the pruner cannot say.
The terms stay apart rather than blending into one number, because that is how winnow savings reads them out — the avoided write and the avoided reads are different quantities and only one of them grows with the length of the session.
winnow savings reads them out. D and T are left symbolic: the model is stated for any result over any number of following turns, and supplying a number for either would be one this site invented.the replay
The table is a simulation
The 8.21% comes from replaying the three no-hindsight rules over 175 historical sessions — what the filter would have done, not a record of anything it did. It is stated here with that attached, because a share of a bill reads like an invoice line and this one is not.
It also describes a floor the filter has left behind. The replay used --min-bytes 2048, inherited from the pruner; the filter now ships 256, because it sends a candidate once in full and the pointer then lives in the cached prefix, so its break-even is between one and two pointer lengths rather than two thousand bytes. Re-measured over one install's 869 transcripts, moving the floor takes the reach from 8.03% to 10.82% and the net up 31% at 224 following turns. The dollar figures below have not been recomputed.
| what was replayed | reaches | share of the bill | sessions where it pays |
|---|---|---|---|
| intake filter | 8.21% | +3.76% | 175 of 175 |
| pruner, tier CB | 10.17% | +3.27% | 97 of 168 |
Running both is possible and nearly pointless. The filter takes the shared mass first, leaving the pruner 2.2% against an unchanged S.
the instrument
What the ledger says it did
winnow savings reads ~/.winnow/filter.jsonl, joins each line to the Claude Code transcript on request_id to recover which session it belongs to and how many API turns followed it, and prices it. The two numbers are not comparable and the command does not try: the simulation is a corpus average, this is one install's ledger over however long it has been on.
It is src/winnow/savings.py — stdlib only, about 575 lines with 34 tests. The readout splits the avoided write from the avoided reads rather than blending them, and names the lines it could not join or could not price.
python -m winnow savings--json for the machine-readable form. The figure is modelled, not billed, and the command says so in its own output: the bytes were never sent, so no invoice line corresponds to them.
The two things it has to get right
The filter is stateless. It re-drops the same result on every later request that still carries it, so a ledger of 1,283 removal events on one install holds 49 distinct results — summing bytes_dropped over lines would report 27× what was removed. The repeats are not removals; they are the 0.1·D·T term, and are priced at 0.1×. De-duplication is on tool_use_id, with a conservative (tool, rule, bytes) fallback for lines written before that field existed.
One API request is one turn, however many records it left on disk. Claude Code writes a response as one record per content-block group — the text, then each tool_use — and stamps every one of them with the same requestId and the same message.usage. Counting records instead of requests inflates both T and the bill it is compared against, by 1.7 to 2.4× on that install's transcripts.
before you run it
It is in your credential path
It relays your auth headers upstream, holds none of its own and logs none — but an operator running it has put a process of their own in front of their own key.
It refuses to start without WINNOW_FILTER=1 for that reason, and forwards the original bytes unchanged on any failure to parse or rewrite: it must not be the thing that breaks a run.
export WINNOW_FILTER=1python -m winnow filter --ledger ~/.winnow/filter.jsonlexport ANTHROPIC_BASE_URL=http://127.0.0.1:8789
The third line is what the proxy prints. Point the client at it and every request goes through the filter.
turning it off
The two ways are not equivalent
Set the toggle blank and restart for the full off. On a running install, what can be turned off is the rewriting and not the proxy: touch ~/.winnow/filter-off and the next request is relayed untouched. Remove the file to resume.
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.
touch ~/.winnow/filter-off