I checked the 90% token cut against 30 days of my own Claude Code. The hook would have fired on 3.5% of it.
This morning I posted about a Spotify plugin that cut one team's Claude Code token spend by 90%, and by dinner the post had 157,000 views and 1,128 bookmarks. That is a lot of people planning to install something. Before I quote-tweeted my own post to keep it rolling, I wanted to know whether the thing would work for me. So I measured it, and the answer is the reason this post exists.
What shunt does
The writeup is by Dimitri Mazmanov at Spotify, and it is awesome, mostly for the section most people skipped.1Portal by Spotify cut my Claude Code token usage by 90%, engineering.atspotify.com, September 2026. The plugin is shunt, in the spotify/portal-ai-plugins marketplace. The setup has three layers. Two PreToolUse hooks fire before every tool call: check-file-size blocks a Read on any file over a threshold, 350 lines by default, and check-bash-read catches cat, head, tail, less and more on the same files. Targeted reads pass, so offset and limit go through and so does cat file | grep. When a hook blocks, its message points Claude at a skill that shows the right invocation, and the skill calls a script that ships the files to a Gemini 2.5 Flash worker and brings back structured bullets. Claude never sees the file. It sees the summary.
He measured it on a Java monorepo across four scenarios and the mean saving on a bulk read was around 90%.
The section people skipped is called "What doesn't work", and it is more candid than most vendor writeups manage. You cannot delegate editing, because the worker's summaries do not carry reliable line numbers. You cannot delegate reasoning, because the worker missed a thread-safety bug that Claude caught in seconds once it had the context. Each delegation is a 10 to 30 second round trip, which is why the threshold exists at all. Under 350 lines the trip costs more than it saves.
And the paragraph I quoted most in the replies: his first version was a block of routing rules in CLAUDE.md. Claude read them and followed them when it felt like it. Every project needed its own copy. The hook replaced that because a hook does not ask.
What I measured
Claude Code keeps every session as a JSONL transcript under ~/.claude/projects/. Every tool call is a tool_use block in an assistant message, and every result comes back as a tool_result in the next user message, joined by an id. So you can replay a month and ask, for every character that entered the context window, which tool put it there.
I took every transcript modified in the last 30 days. That is 574 sessions and 174,109,752 characters of message content, call it 43 million tokens at four characters a token, which is an estimate and not a bill.2Characters, not tokens. Code tokenizes denser than prose, so the shares are close and the absolute token count is loose. Bash results over 30,000 characters are spilled to a file by Claude Code and the transcript keeps a preview, so the Bash figure is a floor.
Where it went:
| Tool | Calls | Characters | Share of context |
|---|---|---|---|
| Bash | 45,583 | 49.6M | 28.5% |
| Read | 4,931 | 24.9M | 14.3% |
| Edit | 6,426 | 2.0M | 1.1% |
| Agent | 683 | 1.5M | 0.9% |
| Write | 2,827 | 1.4M | 0.8% |
| everything else | under 1% each |
The remainder is my prompts, the model's own text and its thinking.
Thirty days of one person's sessions: a homelab, an Obsidian vault, a handful of Python and TypeScript repos, no Java monorepo anywhere. The shares below are what shunt would have done to that. Your Read share could be three times mine and the conclusion flips. The prompt at the top of the post is how you find out.
Now run shunt's two triggers over it.
check-file-size, Read at 350 lines or more. My median Read is 40 lines. 195 of 4,931 reads cleared 350, which is 4.0% of calls, and they carried 24.0% of Read bytes. As a share of everything that entered my context in the month: 3.43%.
check-bash-read, the cat family at 350 lines or more. 2,293 calls, median length seven lines. Five of them cleared 350. Five. They add up to 85,349 characters, which is 0.049% of the month.
I spent this afternoon in the replies explaining the escape-hatch hook to strangers, and it was the reply that got the most likes. In my own transcripts it would have fired five times in thirty days.
Combined, the two hooks would have touched 3.48% of my context. Apply the 90% to that and the plugin saves me about 3.1% of the month, before you subtract the Gemini calls and the 30 seconds per round trip.
Where the tokens actually go
Bash is 28.5% of my month, twice what Read is, across 45,583 calls. The median result is 353 characters. 98.3% of all Bash bytes come from results under 350 lines. Forty-five results in the whole month were big enough to trip a line threshold, and the biggest of those were a strings on a binary and a tail -100 on a log, which is a different bad habit.
By command family the biggest is sed at 4.1 million characters, and sed in a Claude Code session means sed -n '240,420p', the targeted range read that every version of this hook is designed to wave through. Then echo at 3.7 million, cat at 2.8 million, grep at 2.7 million, ls at 1.9 million, git at 0.8 million. Each one is small. There are just a lot of them, and each one sits in the conversation for every turn that follows it.
That last part is where the byte count undersells the problem, and it is also the strongest case for Spotify's design. Every turn re-sends the whole conversation. A 900-line file read in turn three is paid for again on turn four, turn five and turn forty, at cache-read rates if you are lucky. A big read early in a long session costs more than its size. So the elephant is worse than it looks, and if your work is a Java monorepo where the median read is 600 lines, the 90% is yours and you should install shunt tonight.
Mine is a homelab, a vault of markdown and a lot of shell. The reads are short, the shell is constant, and the thing eating the budget is a thousand git status and ls -la and ssh host pgrep a day, none of which any threshold will ever see. Ants need fewer calls: batch five checks into one command, pass the quiet flag before you run it rather than after, and stop re-running a check that already passed. All of which is behaviour. Which is the one thing a hook is bad at and a CLAUDE.md note is worse at.
The part that transfers
The 90% is his. The hook is anyone's.
I run ten PreToolUse hooks and none of them is about tokens. One refuses rm -rf and points at trash. One refuses to kill a process group when the group resolves to 1, because that once meant every process on the machine. One denies an outbound WhatsApp send that is not in Simplified Technical English. One reads every Bash command for an injected instruction before it runs.
The one I would put next to Dimitri's CLAUDE.md story is the voice rule. For months my writing-voice spec was loaded into every session, every turn, as always-on context. Then a partner brief shipped with 88 bold spans in 96 lines, with the rule sitting right there in context the entire time. I moved the spec out of always-on context and into a PreToolUse hook that denies the first voice-bearing write of a session, injects the spec, and allows the retry. It fired on this post before I could land the first line.
A note in CLAUDE.md is advisory. The model reads it, weighs it against everything else in a 200,000-token window, and mostly follows it. A hook returns exit 2 and the tool call does not happen.3Claude Code hooks: a PreToolUse hook that exits 2 blocks the call and hands its stderr to the model as the reason. Exit 0 lets it through. That is the whole protocol, and it is why the block message can point at a skill. Those are different mechanisms, and every rule you have that must not be broken belongs in the second one. That is what 1,128 people bookmarked this morning, whether or not they knew it.
What to do
Run the prompt at the top of this post against your own ~/.claude/projects/ before you install anything. It takes about two minutes on a month of transcripts. If Read comes back at 40% of your context and the median is in the hundreds of lines, you are in Dimitri's world. Install shunt, set SHUNT_MIN_LINES to whatever your p90 read is, and take the 90%.
If it comes back like mine, you have an ant problem, and I do not have a hook for that yet. I have a number, which is where a hook starts.
Related reading on this site: the harness inventory for the list of what a Claude Code setup is actually made of, hooks included; when LaunchAgents attackshippedPractical ApplicationsJan 9, 2026When LaunchAgents Attack: A $100 API Crash Loop StoryHow three duplicate LaunchAgents, a port conflict, and missing cost monitoring created 4,590 restart attempts and $100 in overnight API charges. for the last time I let a token bill teach me something; and debugging million-token meeting notesshippedPractical ApplicationsJan 14, 2026The $221 Bill: Finding and Fixing Million-Token Meeting NotesHow a set-and-forget meeting notes automation quietly racked up costs with 1M-token prompts, and why chunking and caching matter now that the AI honeymoon is over. for what a single oversized read does to a session in practice.
Follow the lab
Get the next experiment
Enjoyed the breakdown on I checked the 90% token cut against 30 days of my own Claude Code. The hook would have fired on 3.5% of it.? New entries land roughly weekly. No digest, no roundup. Just the next build log, when it ships.
Related experiments
Apparatus
1,769 words · 7 min read
- claude-code
- hooks
- token-cost
- model-routing
- measurement