DEV Community

JEONSEWON
JEONSEWON

Posted on

I looked for wasted tokens in Claude Code six times. Five times they weren't there.

The bill keeps climbing. Every span in the trace comes back 200 OK. The dashboard says the agent is healthy. Where is the money going?

I built a small CLI called Clew to answer that. What I found — over six rounds of measurement on public trace data — surprised me. Claude Code itself, session after session, is not the problem. The waste I was hunting is somewhere else. And the most promising detector I built failed hard enough that I had to kill it.

Why I built it

What I actually wanted to see was the black box between agents — the part where one hands off to another and you have no idea what got repeated, re-explained, or thrown away in the gap. But you can't measure what happens between two agents until you can measure what happens inside one. So I started there, with my own Claude Code sessions, and the detour turned out to be the interesting part. Every time I was sure I knew where the waste was, the data said otherwise.

How it decides — no LLM in the loop

Two gates, both deterministic:

  1. Structural gate — group spans by (node, normalized input); groups of >= 2 are candidates.
  2. Identity gate — require sha256(output_A) == sha256(output_B). If outputs differ, NOT flagged.

If the outputs are byte-identical, the pair is a candidate. If they differ — state changed, a retry succeeded where the first failed, the tool returned a slightly different string — the pair drops out. No embedding, no judge, no model in the loop.

On top of each flagged pair, a report-only category label chosen strictly from the tool name:

error_repeat — the response matches an error pattern (agent re-ran with the same wrong args).
side_effect — a state-changing tool (Edit, github-create_pull_request, …) re-invoked with the same arguments.
idempotent — a read-only or declarative tool (Read, filesystem-list_directory, …) called repeatedly.
unclassified — the tool's effect depends on the payload. Bash, PowerShell, local-python-execute, bigquery_run_query live here. The name alone cannot classify them; I don't try.

Labels never change the cascade. The sha256 gate is authoritative; the label is guidance for the reader.

Frozen parameters: phi = 0.514345, N = 2, embedding model pinned to a git tag with a manifest sha256. N = 2 was an arbitrary default at first; I later checked it against N in {2, 3, 5, inf} on RedundancyBench and it turned out F1-optimal (F1 decreases monotonically as N grows). That is the only tuning story in the repo, and it's post-hoc verification, not a knob I turned.

What I measured

RedundancyBench (human-labeled ground truth)

Step-level F1: 0.2642 vs the paper's best LLM-as-judge method at 0.2488. Precision 0.826. Same evaluate.py, same scope, imported directly from their repo.

Honest caveats: the 0.2488 comparison is the paper's cited number — the baseline's prediction files aren't in the repo, so I can't reproduce 24.88% locally. Recall is 0.157. Clew catches one narrow pattern well and deliberately ignores the rest.

trace-commons (28 real public Claude Code sessions)

Full scan, 2026-07-19:

28 / 28 sessions processed, 0 crashes.
10 / 28 flagged as wasteful. 18 / 28 had zero flagged waste.
Aggregate saving potential across all wasteful sessions: $1.01 to $10.12 (cache-hit to cache-miss).

That is a real, low number. The median CC session in this set has no detectable waste at all.

Toolathlon (6,780 trajectories, 22 frontier models × 3 runs)

Now the picture flips. Over 176,270 tool spans, Clew flags 8,042 duplicate pairs.

Self-rebuttal, up front: 47% of those (3,791 pairs) are idempotent — the grey area where "is this really waste?" depends on whether external state changed between the calls. Strip the grey area out and you get:

4,251 pairs (2.41% of tool spans) — about 3× the rate on CC sessions (0.80%).
1,343 side_effect pairs — state-changing tools re-invoked with matching arguments.
Including 459 duplicate email-send pairs. This is detection of duplicate invocations, not confirmation the SMTP server actually delivered twice — the trace can't tell me that. 459 same-argument email-send pairs, in a benchmark, from frontier models.

Toolathlon adapters carry no token counts, so no dollar figure for these. And Toolathlon ships only pass/fail labels, not step-level ground truth. Candidate density varies 54× across models (0.157 – 8.463 per trajectory), but "model X wastes 54× more than model Y" would over-claim — no labels, task-mix and success-rate confounds uncontrolled.

The twist

I built this expecting Claude Code — my daily driver — to be full of waste. So I went looking, one candidate pattern at a time, on real CC sessions:

Normalization gaps — same call, different JSON key order or path format, so the hashes miss each other. Effect: zero. The adapter already canonicalizes before hashing.
File re-reads — reading a file whose content is already in context. Effectively zero real cases. Pre-registered a detector for it anyway; it died. (Next section.)
Dumb retries — failing, then re-running with identical arguments. Almost absent. CC reads the error and changes the arguments.
Context accumulation — this one is real. A file read once stays in the trajectory and gets re-consumed on every subsequent turn, so a single read early in a long session is paid for many times over. But "was this context still needed 100 turns later?" is a judgment call, not a hash comparison. It's outside what a deterministic tool can claim.
Inefficient sessions having more duplication — I expected sessions that burned more tokens per unit of work to also duplicate more. No correlation.
Read↔Edit ping-pong — one file in one session was opened 76 times. Looked damning. It was normal incremental development; each edit touched a different part of the file.

Five of six weren't there. The sixth is real but sits outside the line I drew for myself — no LLM judge, ever.

Then I pointed the same tool at Toolathlon, where agents work with 523 distinct tools instead of CC's 20, and the rate tripled. The pattern: Claude Code by itself is efficient. Waste appears when you stack many tools that look interchangeable to the model. A codebase with ~20 tools rarely gets confused. A benchmark with 523 tools does. That's a claim about tool orchestration, not about any one model's intelligence.

The KILL story

The most interesting failure I've had on this project.

In real CC sessions, the agent sometimes re-reads a file it already read, with no Edit in between. Clear waste, right? I pre-registered a "file re-read" detector: two Read spans on the same canonical path, no writer tool between them, no Bash / PowerShell between them (which could modify the file behind our back).

Pre-registration matters here — the predictions and stop conditions get pushed and PR'd before the results run. K4 threshold: 70% precision on a 30-pair random sample. Written before I saw any output.

Reproduced 2026-07-24. Pool of 209 gated Read pairs. Random seed 42. 30 pairs sampled.

1 of 30 pairs had identical arguments.

The other 29 were the agent reading different slices of the same file — offset=163, limit=100, then offset=260, limit=40, and so on. Normal workflow. Not waste.

The 1 that matched was a password-protected PDF. Both outputs, byte-identical:

PDF is password-protected. Please provide an unprotected version.

That's a failed-tool retry pattern, not a file re-read.

Lenient precision (count the PDF): 3.3%
Strict precision (don't): 0%
Threshold: 70%

KILL. find_reread_candidates removed from src before v0.3.0 shipped. Branch retained for provenance, never merged.

Then the natural follow-up: what if I add an args_equal gate — require offset and limit to match exactly? Across the full 209-pair pool, that gate found 6 pairs. Three real re-reads — every one of them already caught by the existing requery route via structural + sha256. The other three were the same PDF retried three times in one session. Zero new signal.

The narrower detector would have shipped duplicate output for cases already handled. The pre-registration held. The detector stayed dead.

Full write-up — predictions and results in one file, timestamped, so you can see what was pre-committed vs what was measured: REREAD_DETECTOR_PREREG.md

What Clew is and isn't
It diagnoses. It does not fix. The output is a Markdown report you read. What to change in your agent — prompt, cache placement, tool routing — is a call only you can make.
One working pattern, on purpose. repeat / requery. A second pattern (pingpong, reasoning-level — the agent-to-agent thing I originally set out to find) is implemented but has never fired on a real trace format I've validated. I don't advertise what I haven't observed.
Reads what you already have. CC session JSONL, OpenTelemetry SDK-JSON, OpenInference (Phoenix / TRAIL), Toolathlon, RedundancyBench. Cursor and Codex not yet — their local formats are under evaluation.
No measured savings yet. I can detect and estimate. I have zero before/after data from someone who fixed something and watched their bill drop.
239 tests, CI on every PR, frozen params enforced as failing tests. Changing phi requires a documented recalibration, not a nudge.
Try it
pip install "clew-custos[detect]"
python -m clew analyze ~/.claude/projects//.jsonl --out report.md

The bare name clew on PyPI is an unrelated placeholder — install clew-custos. The module still imports as clew.

Repo: https://github.com/JEONSEWON/Clew-by-Custos

What I'd like to know

If you've run agents with a large MCP tool surface — 100+ tools, mixed vendors — and you have a trace archive lying around, does the tool count correlate with your duplicate-call rate? My Toolathlon-vs-CC delta is one data point. Not a law. If your numbers disagree with mine, I'd rather know.

Top comments (0)