# Spotify Cut Claude Code Tokens 90%. I Rebuilt It With 1 Hook

Video: https://www.youtube.com/watch?v=9hrBf9OeJ9k
Article: https://aidive.dev/videos/spotify-portal-claude-code/
Published: 2026-09-11

## Chapters

- [0:00](https://www.youtube.com/watch?v=9hrBf9OeJ9k&t=0s) Ninety percent, and the sentence that sold it
- [0:49](https://www.youtube.com/watch?v=9hrBf9OeJ9k&t=49s) What Portal actually is (and why you can't run it)
- [1:54](https://www.youtube.com/watch?v=9hrBf9OeJ9k&t=114s) Layer one: the hook that blocks instead of asking
- [3:38](https://www.youtube.com/watch?v=9hrBf9OeJ9k&t=218s) Layers two and three: the workers and their numbers
- [5:02](https://www.youtube.com/watch?v=9hrBf9OeJ9k&t=302s) Rebuild, part one: a subagent with a model field
- [6:35](https://www.youtube.com/watch?v=9hrBf9OeJ9k&t=395s) Rebuild, part two: the deny, in today's hook format
- [8:12](https://www.youtube.com/watch?v=9hrBf9OeJ9k&t=492s) The measurement
- [9:58](https://www.youtube.com/watch?v=9hrBf9OeJ9k&t=598s) Keep the block. Don't expect the bill to drop by ninety.

## TL;DR

- Spotify's setup did not cut the bill by 90%: rebuilt in plain Claude Code and measured on Fastify, it cuts the main model's context by 59.6% and the total cost by a third. Spotify's 90% is the mean of three bulk-read rows in estimated input tokens, with no dollar figure and no quality score.
- The pattern is three layers: a PreToolUse hook that denies reads of files over 350 lines, two cheap worker subagents (a Haiku reader, a Sonnet writer), and a three-line rule that tells the main model when to delegate.
- Delegation is slower every time (+65% wall time on average) and can cost more on small tasks: the 45-line test-writing scenario came out 2.6% more expensive.
- Two traps to fix before trusting the hook: hooks also run inside subagents (exempt your workers), and a shell range read such as sed passes straight through.
- The reader's summary carried errors in two runs out of eight; the main model's verification turn caught them, so never skip it.

## Ninety percent, and the sentence that sold it

The Spotify Claude Code setup is a blog post by Dimitri Mazmanov, a product manager at Spotify, with its code on GitHub: he says the configuration his team uses cut his Claude Code token usage by 90%. His first line carries the whole argument: most of what an AI coding agent does is not thinking, it is I/O. Reading five files to answer a question about one method, or writing the twenty-first test file that copies the twenty next to it, burns thousands of tokens with almost no reasoning.

One tweet carried the post to a million and a half views on a single sentence: written rules are a suggestion, a block is not. Hacker News put it on the front page, 271 points and 173 comments, and half of the comments asked the same question: 90% of what? Spotify's own qualifier is "bulk read". This article rebuilds the setup inside plain Claude Code, then measures it, so you know exactly what that qualifier buys you.

## What Portal actually is (and why you can't run it)

Portal is not a router. It is Spotify's internal developer portal, built on Backstage, the developer platform Spotify open-sourced. The relevant feature inside it is called Modes: by Spotify's definition, a mode is a declarative agent that runs on an ephemeral runtime, roughly AWS Lambda for agents. You write the instructions, pick a model, set a temperature, attach tools. Mazmanov built two of them, a bulk reader and a code writer, both on Gemini Flash at temperature 0.2, so both are cheap and boring on purpose.

The routing lives in a Claude Code plugin called Shunt. It is public on GitHub and installs in two commands. Step two, however, authenticates the Portal command line against your Portal instance, and you do not have one. The plugin is public; the thing it delegates to is not.

So the useful move is to forget the plugin and keep the pattern. It has three layers, in his own words: hooks, scripts, skills. Every one of them has a plain Claude Code equivalent, which is what the rest of this article builds and measures.

## Layer one: the hook that blocks instead of asking

Version 1 of the setup was a block of routing rules in the project's instruction file. In Mazmanov's words it "sort of worked": the rules were advisory, not enforced, Claude could ignore them, and every project needed its own copy. Version 2 moves the decision out of the prompt and into the tool layer with two hooks, both firing before a tool call. One watches every file read, the other watches the shell.

The read hook is 33 lines of bash. It reads a threshold from the environment, 350 lines by default, then lets three things through:

- A read with an offset or a limit, because Claude already knows what it needs.
- A file that does not exist.
- A file at or under the threshold, because delegating something small costs more than reading it.

Everything else is blocked, with a message Claude reads instead of the file: this file is that many lines, use the bulk reader skill, and if you need exact content for an edit, reread just that section. The shell hook catches cat, head, tail, less and more on a big file. A piped command passes, because piping into grep is a targeted read.

Mazmanov's point about layering is the important one: even if Claude never reads the skill description, the hook still blocks the expensive read. The skill makes the redirect smoother; the block makes it real. One detail matters later: the script answers with a top-level decision named "block". Keep that word in mind.

## Layers two and three: the workers and their numbers

The workers are two prompts. The reader: "you are a precise code analyst, output structured bullets only, no greetings, no prose, lead every bullet with the exact name, type or line number." The writer: "match the existing patterns, naming and style exactly; output only the code, no fences, no explanations." Without that last line the model wraps everything in Markdown that Claude then has to parse.

Two scripts wrap them. Bulk-read takes a question and file paths and sends them off. Code-write takes a spec and a reference file and writes the result straight to disk, so Claude never sees the generated code. Every delegation is one-shot: a follow-up sends the files again. That is free where it matters, because the corpus goes to the worker and never enters Claude's context.

Layer three is a skill file that tells Claude when to delegate: files over 350 lines, questions across three or more files, large diffs. Its last line is "verify line numbers before you edit".

Spotify's table covers one Java monorepo and three read scenarios. The single-file case drops from about 34,000 tokens to under 6,000, and the mean saving across the three rows is 90%.

| Spotify's benchmark | Value |
|---|---|
| Repositories | 1 Java monorepo |
| Scenarios | 3, all bulk reads |
| Single-file case, before | ~34,000 tokens |
| Single-file case, after | < 6,000 tokens |
| Mean saving | 90% |
| Token estimate | 4 characters per token |
| Writer enforcement | none (only the reader has a hook) |

Two caveats are printed by Spotify itself: tokens are estimated at four characters each, and the writer has no enforcement at all. So the 90% is the mean of three bulk-read rows in estimated input tokens, with no quality score and no dollar figure anywhere. That is the number to test.

## Rebuild, part one: a subagent with a model field

Claude Code ships a built-in Explore subagent, and since a recent release it inherits your main model, capped at Opus, so the "cheap reader" is not cheap anymore. The documentation gives the fix in one sentence: a project subagent named Explore overrides the built-in one and keeps its own model field. One markdown file, a front matter, and the model line says Haiku. That is the bulk reader. The writer is a second file: model Sonnet, tools Read and Write only, and the body is Spotify's own instructions pasted in.

It works because every subagent starts with a fresh, isolated context window. What it reads lands there, not in the main conversation. That is Spotify's one-shot delegation minus the network round trip.

Then comes the part nobody plans for. On Reddit this week, Fable was told to spawn Opus agents and spawned five Fable agents instead: 73% of a weekly limit gone in thirty minutes. The top answer was a hook that runs when the model dispatches a subagent, forces it to choose the model explicitly, and tells it to pick the cheapest one that can do the task. That is hook number three: it watches the Agent tool, and a call with no model is refused with one sentence, "choose the model explicitly".

Spotify's skill becomes three lines in the project's instruction file: files over 350 lines go to the explorer, boilerplate goes to the writer, every agent call sets a model. The blunt option also exists: two environment variables that force one model on every subagent. The honest limit is that the reader is a cheaper model, so what it returns is all the main model knows. The measurement section covers that.

## Rebuild, part two: the deny, in today's hook format

Remember the word "block". Spotify's script returns a top-level decision, but the current Claude Code documentation says something different: a PreToolUse hook returns its decision inside a hook-specific output object, and the field is called permissionDecision. It has four outcomes, allow, deny, ask and defer, and the one wanted here is deny. Whatever the hook writes as the reason is shown to Claude, and if several hooks answer, deny wins.

The rebuilt read hook keeps the same threshold of 350 and the same three exceptions, and instead of "block" returns a deny with a reason that names the Explore subagent and the model to use. One trap the docs state plainly: hooks from your settings also run inside subagents. Without an escape, the Haiku reader is denied on its own reads and can never do its job, so the script checks who is calling and lets the two workers through.

The wiring is one settings file with three matchers, Read, Bash and Agent, each pointing at its script, and the threshold set as an environment variable. In practice a read on a file of 1,090 lines comes back as an error with the written sentence: delegate this read to the explorer, model Haiku. The delegation then follows: the main model counts the lines first, calls the explorer with the model set to Haiku, and the bullets come back, each with a line number. Three turns, 44 seconds.

Spotify's line holds: the layering means the system degrades gracefully. The instruction does the routing, the hook is the net. The net has a hole, though. A model that wants the whole file can chunk it with offset and limit, which passes, or dump it through the shell with a sed range, which this hook does not catch. The measurement counts both.

## The measurement

The test repository is Fastify, the Node web framework: 294 files, 63 of them over the threshold. Two identical clones, the only difference being the .claude folder and the rule file. Main model Opus, the CLI default; reader Haiku; writer Sonnet. Single-prompt sessions, no follow-ups, every scenario run twice per configuration, sixteen runs in total. The four scenarios are the same as Spotify's: the exports of one big file, three files and how they call each other, a source file against its test, and a new test file written to disk from an existing one.

| Scenario | Main context, without | Main context, with | Change | Total cost, without | Total cost, with | Change | Duration, without | Duration, with | Change |
|---|---|---|---|---|---|---|---|---|---|
| One big file | 88,693 | 51,552 | -41.9% | $0.139 | $0.087 | -37.8% | 22 s | 44 s | +100.8% |
| Three files | 357,166 | 73,440 | -79.4% | $0.581 | $0.218 | -62.4% | 52 s | 129 s | +149.9% |
| Source vs test | 303,808 | 114,136 | -62.4% | $0.451 | $0.374 | -17.1% | 93 s | 125 s | +33.6% |
| New test file | 143,432 | 121,818 | -15.1% | $0.295 | $0.302 | +2.6% | 66 s | 87 s | +32.2% |
| All four | 223,274 | 90,236 | -59.6% | $0.366 | $0.245 | -33.1% | 58 s | 96 s | +65.3% |

Main context, the tokens the expensive model actually saw, is the first column that matters. On the three-file question it drops by 79%, and across all four scenarios by 59.6%. The bill drops less, a third overall, because the reader's own tokens are not free, and on the small test-writing task the bill went up by 2.6%. Time goes the other way: 58 seconds on average without the setup, 96 with. Delegation is slower every time.

Quality is where the two configurations differ most. Without the setup, the main model dumped files through the shell without line numbers and counted by hand, producing wrong line numbers throughout: a function reported at line 149 actually sat at line 156. With the setup, one run in four took the reader's summary at face value and carried three false claims, one of them a function the reader said the route file never calls, when it does, at line 553. All four generated test files pass, and the deny hooks fired zero times in sixteen runs: with the rule file present, the main model checked the line count and delegated on its own every time.

One more thing from the traces: without the rule, the main model never used the Read tool. It read everything through the shell, and a shell range read costs the same tokens and passes the hook. So Spotify's table says 90; this one says 60 on context and a third on the bill.

## Keep the block. Don't expect the bill to drop by ninety.

Three things are worth keeping: a project Explore subagent on Haiku, a three-line rule in the instruction file, and the read hook as the safety net. The measured result is 60% less main context, a third off the bill, and two thirds more wall time.

Before trusting the hook, fix two things. Hooks run inside subagents, so exempt your workers. And the shell hole: the bash hook catches cat, head and tail, but a range read passes, and the main model used exactly that when it had no rule.

Spotify's own limits stand. You cannot delegate editing and you cannot delegate reasoning; the worker missed a thread-safety bug Claude caught in seconds, and each delegation is a round trip. The Hacker News skeptics were right about one thing too: input tokens are not the bill. Output tokens cost more, and this setup does nothing for them.

Who saves depends on how you pay. On the API, a third off. On a Pro or Max plan, the same setup moves your five-hour and weekly windows, not dollars. Mind the threshold as well: below it, delegation costs more than it saves, and the 45-line test case is the proof at plus 2.6%. Finally, in two runs out of eight the reader's summary carried errors, and the main model's verification turn caught them. Skip that turn and those mistakes reach your edits.

## FAQ

### Did Spotify really cut Claude Code token usage by 90%?

Spotify's 90% is the mean of three bulk-read scenarios in a Java monorepo, counted in estimated input tokens at four characters per token, with no quality score and no dollar figure. Rebuilt with plain Claude Code on Fastify, the same idea cut the main model's context by 59.6% and the total cost by 33%.

### What is Spotify's Portal and can I use it with Claude Code?

Portal is Spotify's internal developer portal built on Backstage; its Modes feature runs declarative agents on an ephemeral runtime. The Shunt plugin that routes Claude Code to those agents is public on GitHub, but it authenticates against a Portal instance you do not have, so you can only copy the pattern, not run the plugin.

### How do I make Claude Code delegate big file reads to a cheaper model?

Create a project subagent named Explore with the model field set to Haiku, which overrides the built-in Explore agent that now inherits your main model. Add a three-line rule to the project instruction file (files over 350 lines go to the explorer, boilerplate goes to the writer, every agent call sets a model) and a PreToolUse hook on Read that denies large reads with a reason naming that subagent.

### How does a PreToolUse hook block a tool call in Claude Code today?

The hook returns its decision inside the hookSpecificOutput object, in a field named permissionDecision, with four possible values: allow, deny, ask and defer. The reason string is shown to Claude, and when several hooks answer, deny wins. Spotify's script uses an older top-level decision named block.

### Does delegating reads to a Haiku subagent make Claude Code cheaper?

On the API it took a third off the bill across four scenarios, but the reader's own tokens are not free: on a 45-line test-writing task the total cost rose by 2.6%. On a Pro or Max plan the saving shows up as quota in the five-hour and weekly windows, not as dollars, and output tokens are untouched.

### Why do Claude Code hooks fire inside subagents?

Hooks defined in your settings run for every agent, including the subagents you dispatch. A read-blocking hook therefore denies the Haiku reader its own reads unless the script checks who is calling and lets the worker agents through.

## Sources

- [Portal by Spotify cut my Claude Code token usage by 90%](https://engineering.atspotify.com/2026/9/portal-by-spotify-cut-my-claude-code-token-usage-by-90) — Spotify Engineering
- [Shunt plugin (spotify/portal-ai-plugins)](https://github.com/spotify/portal-ai-plugins/blob/main/plugins/shunt/README.md) — GitHub
- [check-file-size hook source](https://github.com/spotify/portal-ai-plugins/blob/main/plugins/shunt/hooks/check-file-size) — GitHub
- [Portal Modes documentation](https://backstage.spotify.com/docs/portal/core-features-and-plugins/aika/modes) — Spotify
- [Claude Code hooks reference](https://code.claude.com/docs/en/hooks) — Anthropic
- [Claude Code subagents](https://code.claude.com/docs/en/sub-agents) — Anthropic
- [Hacker News discussion of the Spotify post](https://news.ycombinator.com/item?id=49571465) — Hacker News
- [Fable spawned five Fable agents instead of Opus (r/ClaudeCode)](https://www.reddit.com/r/ClaudeCode/comments/1w9fdnn/) — Reddit
