The home page shows what each part does. This one says where the numbers come from, what each guard actually stops, what ships off, and which of these are gaps rather than guarantees.
Three sources can tell you what Claude cost, and UsageFoundry keeps them apart rather than adding them up: the transcripts Claude Code writes locally, the Admin API if you give it a key, and OTLP if you are exporting it. Each answers a different question, and summing any two double-counts the same work.
exact
Token volumes and dollar costs derived from the transcripts. They are the numbers in the file, read incrementally from the last byte offset and de-duplicated.
Anything derived locally. Cowork, the desktop app and the web app spend the same limits and leave no transcript here, so consumption is at least this, never at most.
Tool composition. A tool_result carries no usage block, so it is measured in characters, with its own dedupe key and its own rollup.
Why the ceilings are in dollars
Cache reads bill at a fraction of the input rate — a tenth on most models, and 0.025× on Claude Fable 5.1 and Claude Mythos 5.1, because the multiplier is a property of the model rather than a constant. A Claude Code workload is mostly cache reads: UsageFoundry's own docs/limits-and-accuracy.md puts the share at around 98% on a corpus it does not name; the one reading in the product that carries a sample says 92%, across 90 deduplicated usage frames on one install, in src/lib/pricing.ts. Nothing below turns on which of those is right. Denominate a limit in raw tokens and the same work reads wildly differently depending on which number you picked:
Same work, 3.2× disagreement. Both readings are UsageFoundry's own worked example in docs/limits-and-accuracy.md — one real usage, which the source neither sizes nor dates. Cost is the primary metric everywhere here for that reason.
A measured disagreement
On one machine the provider reported 5.0% of the 5-hour window used. The dashboard, working from $8.49 of local spend against a $650 ceiling, showed 1.3%.
The arithmetic was not the problem: cross-checking 4,995 turns against Claude Code's own per-request OTLP cost put pricing.ts within 0.8% in aggregate. The ceiling was. Both readings are UsageFoundry's own account of that one machine, in docs/limits-and-accuracy.md; neither appears in docs/verification.md, where the product keeps what it has re-checked.
A different disagreement from the 3.2×, not a second estimate of it: that one is two ceiling units read against the same usage, this one a guessed denominator read against the provider’s own figure. Headroom is reserved for what happens outside the terminal, capped at 95%.
A window with no ceiling configured is drawn hatched, not at 0%, because those are different states and one of them would be a lie. For the same reason a budget guard expressed as a fraction of a limit is refused outright when there is no limit to take a fraction of.
between-cycles, live or live-resume — see below. None of them is a hard cap.
At least one of maxIterations and maxDurationMinutes is required. A loop with no monotone terminus has no reason to stop, and a spend limit is not one — it can be raised.
Three modes, one shared caveat
between-cycles
Checks before starting the next cycle. Loses nothing, overshoots by at most the cycle already in flight.
Interrupts, then picks the same session back up rather than starting clean.
Concurrency multiplies the overshoot rather than sharing it: maxRunCostUSD is per run, so three runs at $5 each is a $15 worst case.
When the provider says no
A rate limit at high concurrency is your own request rate, so retrying in seconds is three more waves into the condition that caused it. A refused cycle waits 30 seconds, then 2, 5 and 10 minutes, each rung jittered, and gives up after five attempts — roughly 17 to 26 minutes.
The 5-hour window is the only limit this app’s own guard parks a run for, because it is the only one that refills on its own. Spend, cycles and the clock move one way, so those end a run instead. A refusal that comes back from the provider naming the weekly or the Opus limit parks it too.
Two container-wide caps bound the fleet: Runs at the same time ships at 4, and Other Claude processes at the same time — reviews, chat turns, orchestrator blocks — ships at 2. Neither bounds the total, because a freed slot refills immediately. The rolling 24-hour install limit is the one that would bound the sum, and it ships unset.
A run's checkout is a real git worktree on a branch of its own, under .uf-worktrees/ inside the mount, cut from your last commit and seeded with the .env files git would not have carried. There are 64 slots per repository, reused between runs. They are usable from inside the container; they are not a second copy for you to open.
before merging
Your checkout has to be clean and still on the branch the run was cut from. Otherwise landing refuses rather than guessing.
Strictly one merge at a time within a repository, oldest batch first. Different repositories land at once, up to four.
┌─ gap ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
A concurrent run’s checkout is inside the trust boundary of every other run in the container: the isolation is of commits, not of files. The security docs list this as a gap, and that is the honest reading — not a check you are confirming.
Takes a brief rather than a task. Headless Claude Code under a ten-minute bound reads the repository and emits runs — a title, a task, a folder and what each waits on. It starts them; it does not propose them, which is why the fan-out cap is required.
Re-runs a task pass after pass. A pass is not a work cycle — a pass is a whole run. It unrolls rather than loops, and stops on DONE, on a pass that did not complete, on the pass cap, or on the spending limit across passes.
No agent, no task. Lands each branch onto the base its own run recorded when it started; the target is never named in the graph.
A workflow carries one spending limit across the whole graph, stops on the 5-hour window, and can be put on a schedule — which is the case where nobody is present to notice it started.
┌─ not a guard ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
The graph picks what work to do. Something a person wrote — settings, hooks, permissions — picks what an agent may do. A block is a piece of work, not a boundary, and treating it as one is how an unattended graph surprises you.
A plugin is a directory in one of your mounted workspaces. UsageFoundry finds it, lists it under Settings → Plugins with which of hooks, agents, skills, commands and MCP server it carries, and switches it on the moment you press the switch rather than on Save. Nothing is copied into ~/.claude.
when it takes effect
From the next work cycle onward, runs already in flight included.
Winnow is wired in at two points, and they are different mechanisms rather than two settings for one — different copies of it, too. The pruner runs the one bundled into the image at a pinned ref: WINNOW_REPO and WINNOW_REF are build arguments, not environment, because nothing inside the container reads them. The filter takes whichever copy it finds first: WINNOW_FILTER_PATH if you set one, a checkout under the workspace mount if there is one, and otherwise the same pinned copy the image already carries. It ships blank, so a stock install runs the pinned one.
The intake filter, in front of the wire
WINNOW_FILTER=1 makes the entrypoint start a loopback proxy inside the container and export ANTHROPIC_BASE_URL pointing at it, so every agent this container spawns talks through it. One variable rather than two: the URL is never set by hand, because a URL pointing at a proxy that was never started fails every request inside a tool call.
A tool result a rule marks as spent is sent in full on the one request the model acts on it — placed past the last cache_control breakpoint — and is replaced by a pointer on every request after that. The position of that full send is the whole mechanism: past the last breakpoint, the API never writes the result to the prompt cache, so there is nothing to read back on any later turn. Nothing is edited, so no cached prefix is invalidated either.
Only the rules that need no hindsight can fire here — C1, C3 and B2. The other three have to see the conversation's future, and a policy that did would change the prefix under the cache.
It relays your auth headers upstream and holds none of its own, but it is a process of yours in front of your own key. It refuses to start without the switch, and forwards the original bytes unchanged on any failure to parse or rewrite.
The pruner, at the cycle boundary
A prune edits the transcript, and an edit to a cached prefix invalidates everything after the cut. At the boundary between one work cycle and the next that cost is refunded, because --resume was going to rewrite the prefix anyway. It is the one moment the edit is free, and it is why the prune happens there rather than at an occupancy threshold inside a live cycle.
A cycle whose context passes 200,000 tokens is ended early so it can be pruned. The trigger started at 167,000, where --autocompact 200000 used to fire: matching it was deliberate, because moving the trigger in the change that swapped the mechanism would have left nothing able to tell which half a later reading was seeing. It has since moved.
Compaction and pruning are not complements. Compaction replaced the conversation with a model-written summary; a prune removes tool output and keeps the conversation, so it shrinks less and forgets less. The docs say not to reinstate autocompaction beside pruning, because the CLI would summarise a conversation moments before this app ended the cycle to prune it, and the run pays for both.
the ledger
The filter appends one JSON line per rewritten request to /var/lib/winnow/filter.jsonl — the named volume, not winnow's own ~/.winnow, which in this container is the writable layer a restart discards. It is the one file the app reads and nothing in the app writes.
Only the message a transcript record carried is counted; the envelope around it — toolUseResult most of all — was never in anybody's context. Winnow frees more file than it removes from what is sent, and its own token readout can report zero for a prune that took out a large fraction of the context.
standard and aggressive. gentle is excluded because its one strategy that fires on an ordinary session is metadata-strip, which orchestrator-safe mode excludes by name: it deletes the usage, costUSD and duration fields every window and every budget guard here is computed from. Offering it would be a control that reads as on and provably does nothing.
The filter re-drops the same result on every later request that still carries it. One removal appears on many lines, so counting lines is not counting removals.
WINNOW_REF defaults to one commit, and that is the copy the pruner runs. Building with it empty leaves winnow out of the image, and context pruning then reports itself unavailable rather than quietly pruning nothing.
ANTHROPIC_BASE_URL → 127.0.0.1:8789
┌───────┐ ┌────────┐ ┌──────────────┐
│ agent │──→│ filter │──→│ API · cache │
└───────┘ └───┬────┘ └──────────────┘
↓
a spent result: in full once, after
the last cache_control break, so the
API never caches it; a pointer after;
one line per rewrite → filter.jsonl
═════════════ cycle boundary ═════════════
cycle N ████████████████ ends here
prune ████████░░░░░░░░ ░ tool output
cycle N+1 ████████ resumes
Diagram in two bands. In the upper band an agent's requests go through a filter to the API and its cache, with the filter branching off to note that a spent result is sent in full once after the last cache_control breakpoint so the API never caches it, is a pointer on every request after that, and puts one line per rewrite in filter.jsonl. Below a cycle-boundary rule, a bar for cycle N is shown at full length, a prune removes its right-hand portion, and cycle N+1 resumes the shorter remainder. Illustration. The filter acts on the request; the pruner acts on the transcript, between cycles, with no session live.
┌─ shipped state ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Both are off. contextPruning is false in the defaults, and a blank WINNOW_FILTER is what ships — the toggle has to be set and the container restarted.
On a running install, what can be turned off is the rewriting rather than the proxy: touch /var/lib/winnow/filter-off and the next request is relayed untouched. Killing the process is not the off switch — ANTHROPIC_BASE_URL is fixed in a client's environment when it starts.
┌─ what changes ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Pruning is the only thing bounding a cycle's context. With it off, a long cycle runs to the model's whole window; there is no --autocompact on the argv to catch it. The product ships it off anyway and says so, rather than offering it as a third way of spending less.
What a cut is worth, and why the answer is not obvious
Removing half a conversation does not halve the bill. It is not obvious that it lowers it at all. The conversation sits in the API’s cached prefix, where a read bills at a tenth of the input rate on most models, and matching is exact and prefix-ordered — so an edit invalidates everything after the cut point and forces a full-price rewrite of it.
With S the suffix as it stood before the cut and D what came out of it, at a tenth the edit pays 1.9·S − 2·D once and earns 0.1·D back on every later turn. It breaks even after 19·(S/D) − 20 further turns. The ratio decides and absolute size cancels: a big session is not automatically worth pruning. Cut half the suffix and it pays in 18 further turns; cut a tenth and it needs 170. How many sessions run that long, this page cannot tell you. The one figure taken on it came off one install's transcripts, which this repository does not hold: of 11,422 main-thread turns in the 80 sessions that reached a hundred turns, 807 sat at index 160 or past it. That counts turns, not sessions, and index 160 is a position within a session rather than 170 turns of life after a cut, so it bounds neither. The 170 is arithmetic; how often it is reached is unmeasured.
The 2.0× is a measurement, not a price list lookup. A cache write bills at 1.25× at the five-minute class and 2.0× at the one hour; this arithmetic uses the one-hour figure because across 26,194 turns of one install every main-thread turn wrote at it. Winnow's own docs keep the earlier version on the record, where the 1.25× was assumed from the documentation and invalidation came out about 40 per cent too cheap — which is exactly the mistake the measurement exists to catch.
The 0.1× carries a limit of a different kind: it is the model's property, not a constant. A read bills at a tenth of the input rate on every model UsageFoundry's price table knows but two — Claude Fable 5.1 and Claude Mythos 5.1 read at 0.025×, a quarter of that, taken from the published pricing page rather than from anyone's meter. So the 19 and the 20 are what a tenth gives, and a run on either 5.1 model breaks even at some other number. That is why the product resolves the rate off the model it priced — cacheReadMultiplierOf, the only thing there allowed to read it — rather than off a constant sitting in four places at once.
T* = 19·(S/D) − 20 further turns cut S/D0 ── turns ─── 18 ▸ T* ─────────────────────────────────────────── a tenth 10▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▸ 170 a quarter 4▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▸ 56 a half 2▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 18 two thirds 1.5▓▓▓▓▓▓▓▓▓░░░░░░░░░ 9 ─────────────────────────────────────────── at a boundary 2░░░░░░░░░░░░░░░░░░ 0 the same half cut, at a cycle boundary intake filter —────────────────── none nothing is edited, so nothing is paid
Illustration of when a context cut pays for itself. Bars measure the further turns a cut must survive, against a horizon of 18. Removing a tenth of the suffix needs 170 turns and removing a quarter needs 56, both past the horizon; removing a half needs exactly 18 and removing two thirds needs 9. The same half cut taken at a work-cycle boundary 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 product's own: a cache read bills at 0.1× on every model but Claude Fable 5.1 and Claude Mythos 5.1, the one-hour write this install measured 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.
There is one moment when the edit is free. At the boundary between one work cycle and the next, --resume was going to rewrite that suffix anyway, so the 2·D term is refunded and the cut costs nothing. A cycle ended early to make a boundary does not get that refund; it manufactured the boundary and pays for it. The trigger is priced rather than decorative. Both paths are gated on the arithmetic; the refund only buys the boundary a longer horizon, because it covers the cost of the edit and says nothing about whether it earns.
The gate is 18 further turns, which is not a taste — it is the break-even for cutting exactly half. A cut needing longer is a bet on how many turns a run has left, and nothing in the app knows that. The safe direction is to leave the context alone: an unpruned cycle keeps paying cache reads, the cheapest rate on the bill, where a cut that never pays back has already spent the invalidation at 2.0×.
S is the suffix as it stood before the cut, including the part about to be removed. The after figure is off by exactly D — negligible on a small cut and enormous on a large one — so it flatters precisely the cuts that do not pay.
The filter pays on the first request
The intake filter has no break-even term, because nothing is edited and no prefix is invalidated. Baseline, a result of D tokens is written to cache once at 2.0× and read back at 0.1× on every later turn. Filtered, it is sent uncached exactly once at 1.0× and is a pointer thereafter. That one uncached send is a real cost, and the three terms are drawn separately rather than netted — a single blended number would hide it, and a reader would have no way to check the claim. The 0.1× is the model's property here too: on the two 5.1 models a read is 0.025×, so the 0.1·D·T term shrinks to a quarter. That moves what the filter is worth and not whether it is worth it — the write alone is dearer than the uncached send, at any read rate.
On winnow's own corpus it reaches less than the pruner does, because only the rules needing no hindsight can fire on the wire. What separates the two there is variance rather than size: the filter cannot come out negative.
per result of D tokens, T turns later
───────────────────────────────────────────
baseline 2.0·D cache write, 1h class
+ 0.1·D·T a read, on each turn
───────────────────────────────────────────
filtered 1.0·D sent uncached, once
0 a pointer, after that
───────────────────────────────────────────
no break-even term — nothing is edited
read rate is per-model: 0.1× here, 0.025×
on Claude Fable 5.1 and Claude Mythos 5.1
What one tool result costs with and without the intake filter. Baseline: a cache write at 2.0 times D, plus a cache read at 0.1 times D on each of T later turns. Filtered: 1.0 times D sent uncached once, then nothing. There is no break-even term because nothing is edited. The read rate is a property of the model: 0.1 times as drawn here, and 0.025 times on Claude Fable 5.1 and Claude Mythos 5.1. D is the result's tokens and T the turns that followed the request it first appeared on. Nothing here is an example size: the multipliers are the only figures in the drawing, and the read rate is the one of them that varies by model, which is why the drawing says which rate it is at.
┌─ not money you can watch arrive ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
The filter's saving is a counterfactual, and it is never a cost source. Every meter in this app is priced from usage frames, and a usage frame is the API's report of the request it actually received — the one the filter had already rewritten. So the money is already absent from every figure beside it. It reaches no meter, no guard, no window and no run total.
┌─ the two halves overlap ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Three rules fire in both, and the filter takes that mass first — it sees the request before Claude Code has finished writing the transcript the pruner reads. So a prune can count tokens the API never saw and price re-reads that were never going to happen. Measured on 2026-08-24 across one install's ten largest transcripts that is 4.06% of pruned tokens — corpus-weighted, from per-file shares running 0.00% to 9.92% — and it is an upper bound: correcting it needs a tool_use_id on each ledger line that winnow does not write yet. The figure is UsageFoundry's own, derived in docs/verification.md.
The product’s own card adds the two halves rather than showing two figures nobody can combine. The overstatement is recorded where it was measured, not printed beside the total.
Every figure below is UsageFoundry's own reading, recorded in the product's docs/verification.md, and all of it comes from one machine — the operator's own install.
a ledger line is a request, not a removal
The filter is stateless: it re-drops the same result on every later request that still carries it. On one install's ledger at 125 lines, read once on 2026-08-24, 372 drop or defer occurrences resolved to 15 distinct results — summing the file would have reported 24.8× what was actually removed. That factor is not a constant; it is roughly how many requests a result survives, so it grows with session length. The repeats are not removals, they are the 0.1·D·T term, and they are priced as reads.
Verified against four prunes on one install on 2026-08-24, at 167,326–169,283 tokens, removing 29.1–52.8%. Removal is accurate to about 3% against the observed drop in resident context, with per-receipt errors running +1.2% to −6.5% rather than symmetrically. Those four are receipts 1 to 4 of the 54 taken up to 2026-08-28, of which 52 came from the early end and 2 from a boundary — which is why the boundary path is the thinly evidenced one.
It was first charged against the tokens left after the prune — 405,049 across those four, where the resumes actually wrote 485,828, because the resume also rewrites the system prompt, the tool definitions and CLAUDE.md, none of which the transcript holds. That basis was 16.6% under, which is not the same percentage as what it moved the four-prune net by: +$4.39 displayed against +$3.58 corrected. It is now priced off the first billed turn's own cache_creation_input_tokens — a reading rather than a model — which has unit tests behind it and has not been re-observed against a fifth prune.
no independent implementation has checked the filter's figure
The cross-check against winnow's own winnow savings --json has not been run. It was not available in the checkout the reading was built against; the pinned build carries it. The reading has been checked against its own unit tests and against the raw ledger counted a second way, and against nothing else. Every figure it produces is a floor — unjoined requests, unpriced models and deferred-but-never-dropped results are all excluded and all counted on the card.
┌─ winnow's own corpus, not this product's ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Winnow reports its intake filter at +3.76% of the bill, positive in 175 of 175 sessions — by construction, since nothing cached is ever edited — and its tier-CB pruner at +3.27%, paying in 97 of 168, a ratio of 1.1×. Those are winnow's numbers, on winnow's corpus, and they are a simulation: three no-hindsight rules replayed over 175 historical sessions to find what the filter would have done, not what it did.
They are bounded by what produced them — one operator's own sessions, a convenience sample, and tokens estimated at one per four bytes. The pruner's figure carries a bound of its own: winnow calls +3.27% optimistic, because the simulation cuts every session at its earliest strippable result, where the whole of it is still ahead of the cut. Winnow's own spec sets the target at 15% and calls this short of it. UsageFoundry has not reproduced either figure and does not claim them.