Route bulk file reads and boilerplate generation from Claude Code to a cheaper Claude model. A vendor-free port of Spotify's shunt plugin: same hooks, same skills, no Portal account required.
Spotify's engineering team reported a roughly 90% cut in Claude Code token usage on bulk-read scenarios by intercepting large file reads with a PreToolUse hook and handing them to a cheaper worker model. The worker reads the file in its own disposable context and returns only an answer. The file never enters the expensive model's context window.
Their plugin, shunt, is open source, but its transport is hard-wired to Spotify's Portal CLI and AiKA modes. If you are not a Portal customer you cannot use it.
shuntkit keeps the hooks and skills and replaces the transport. The default worker is Claude Haiku 4.5, called through the Claude Code CLI you already have logged in. No new vendor, no new API key, no new bill.
Claude (Opus / Sonnet) shuntkit Worker (Haiku)
ββββββββββββββββββββββββ ββββββββ ββββββββββββββ
Read big_file.py ββββββββββΊ PreToolUse hook: 1200 lines > 350
ββββββββββ deny: "use /bulk-reader"
/bulk-reader
--question "..."
--paths big_file.py βββββββΊ secret guard, payload cap
shuntkit read ββββ files + question βββββββΊ claude -p --model haiku
(isolated session,
no tools, no CLAUDE.md)
ββββββββββ answer + verified (file:line) citations ββββββ
Two hooks fire before every tool call:
| Hook | Blocks | Lets through |
|---|---|---|
Read |
Whole-file reads over the threshold (default 350 lines); offset-only reads that would return more than the threshold |
Small files; genuine slices with limit; images, PDFs, notebooks |
Bash |
cat, nl, less, more, bat on large files; sed -n 'A,$p' and sed without -n; a leading cd dir && is followed |
Pipes, redirects, head/tail with a bounded count, sed -n 'A,Bp' slices, grep/rg |
Genuine slices of a large file are charged against a per-session slice budget (default 700 lines per file). Once spent, further slices are blocked until the file has been delegated once. This stops a blocked file from being reassembled piece by piece.
When a read is blocked, the hook's message tells Claude exactly which command to run instead. Claude does not need to have read the skill description for the redirect to work.
You need Claude Code installed and logged in, and Python 3.10 or newer on macOS or Linux.
uv tool install shuntkit # or: pipx install shuntkit / pip install shuntkit
cd your-project
shuntkit install # registers hooks, skills and the bulk-reader agent in ./.claude
shuntkit doctor --probe # one tiny Haiku call to confirm it worksRestart Claude Code. Done.
shuntkit install is per project by default: it writes to ./.claude in the current directory and only that repository gets the hooks. Commit the files to share them with your team; each teammate needs shuntkit on their PATH. To turn it on for every project on your machine instead:
shuntkit install --user # writes to ~/.claude with the absolute path of the executableIf you launch Claude Code from an app rather than a terminal and shuntkit is not on that PATH, pass --command "$(command -v shuntkit)" to a project install.
cd your-project
shuntkit install --host codex # writes the Bash hook into ./.codex/hooks.json (or ~/.codex with --user)Then open Codex in that directory and run /hooks to review and trust the hook; Codex does not run untrusted hooks. Project hooks also need the project itself to be trusted, which Codex asks about on first open.
Codex has no Read tool, so only the Bash hook applies, and it recognises the shell forms Codex uses to read files (sed -n, cat -n, nl). Blocked reads point at the shuntkit read command directly; there are no skills or subagent on this host. Delegation still runs through the Claude CLI or the Anthropic API, so one of those must be set up. Codex plugins cannot ship hooks, so there is no plugin form for Codex.
Inside Claude Code:
/plugin marketplace add melonkernel/shuntkit
/plugin install shuntkit@shuntkit
The plugin form needs only python3 on your PATH; nothing is pip-installed.
shuntkit uninstall # removes exactly what 'install' added to ./.claude
shuntkit uninstall --user # same for ~/.claude
shuntkit uninstall --host codex # same for ./.codex (add --user for ~/.codex)You mostly do nothing. Claude hits a large file, gets redirected, and calls the skill. The skills are also usable directly:
# Ask a question across files without loading them into Claude's context
shuntkit read --question "Which functions mutate global state?" --paths src/*.py
# Generate boilerplate that matches an existing file's conventions.
# --reference is the style to copy; --context is the code the output is about.
shuntkit write --spec "Unit tests for OrderService covering cancel and refund" \
--reference tests/test_user_service.py \
--context src/orders/service.py \
--target tests/test_order_service.pyEach call prints a one-line summary to stderr:
[shuntkit: ~18,400 tokens kept out of context | worker claude-haiku-4-5 used 19,812 in / 310 out | worker cost $0.0214 | 6 verified citations]
shuntkit stats totals these over time. shuntkit doctor shows the active configuration.
The worker is asked to quote code fragments verbatim. shuntkit then looks each quote up in the delegated files. A quote found exactly once gets a (file:line) suffix computed locally, so Claude gets line numbers it can trust without reading the file:
- Config is loaded by `def load_config(path):` (config.py:3), which calls `parse(fh.read())` (config.py:5)
Quotes without a suffix are unverified, and the skill tells Claude to confirm them with a targeted read before editing.
shuntkit read and shuntkit write refuse to send files that look like secrets: .env*, *.pem, *.key, id_rsa*, credentials*, *.tfstate, anything under .ssh/ or .aws/, and any file whose first 8 kB contains a PEM private key block. The refusal names the files. Set SHUNTKIT_SECRET_GUARD=off to bypass it.
Everything is an environment variable. Set them in your shell, or under "env" in ~/.claude/settings.json.
| Variable | Default | Meaning |
|---|---|---|
SHUNTKIT_MIN_LINES |
350 |
Files at or under this many lines are read directly. SHUNT_MIN_LINES (upstream's name) is honoured too. |
SHUNTKIT_SLICE_BUDGET_LINES |
2 Γ MIN_LINES |
Lines of one large file Claude may read in slices per session before it must delegate. 0 disables the budget. |
SHUNTKIT_SANCTION_TTL_SECONDS |
14400 |
After a file is delegated, its slices are unrestricted for this long. |
SHUNTKIT_MODEL |
haiku |
Worker model. Any alias or id the Claude CLI accepts (haiku, sonnet, claude-haiku-4-5, ...). |
SHUNTKIT_TRANSPORT |
claude |
claude (Claude Code CLI, uses your login) or anthropic (Anthropic API via the official SDK). |
SHUNTKIT_DELEGATE |
cli |
What blocked reads are redirected to: cli (the shuntkit read command) or subagent (the bulk-reader Haiku subagent). Ignored on Codex. |
SHUNTKIT_HOST |
claude |
claude or codex. Changes the wording of block messages. shuntkit install --host codex writes hooks that pass --host codex explicitly, so you rarely need this. |
SHUNTKIT_SECRET_GUARD |
on |
Refuse to delegate files that look like secrets. |
SHUNTKIT_CITATIONS |
on |
Attach (file:line) to quotes found exactly once in the delegated files. |
SHUNTKIT_TIMEOUT_SECONDS |
180 |
Ceiling for one worker call. |
SHUNTKIT_MAX_PAYLOAD_BYTES |
600000 |
Refuse requests larger than this (about 150k tokens, under Haiku's 200k window). |
SHUNTKIT_CLAUDE_BIN |
claude |
Path to the Claude CLI if it is not on PATH. |
SHUNTKIT_DISABLED |
unset | Set to 1 to make both hooks allow everything. |
SHUNTKIT_STATE_DIR |
~/.local/state/shuntkit |
Where usage and slice-budget state are written. |
pip install 'shuntkit[anthropic]'
export SHUNTKIT_TRANSPORT=anthropic
export ANTHROPIC_API_KEY=sk-ant-... # or: ant auth loginUseful if you want worker calls billed to an API key rather than your Claude Code plan, or if you run shuntkit outside Claude Code.
export SHUNTKIT_DELEGATE=subagentBlocked reads are then redirected to the bulk-reader subagent that shuntkit install (or the plugin) registers. It runs on Haiku with only Read, Glob and Grep, and the hooks exempt its tool calls. This avoids the CLI round trip but skips the secret guard, payload cap, citations and usage log, because the agent reads files itself. See docs/design.md for the trade-offs.
Where it helps. First reads of large files, and questions that span several files. The worker's answer is typically a few hundred tokens; the corpus it replaced is typically tens of thousands.
Where it does not. Files under the threshold, edit-heavy work that needs exact line numbers beyond what citations provide, and anything requiring frontier-model reasoning. Spotify's own write-up notes the worker "found surface-level patterns but missed a subtle thread-safety bug". Debugging, architecture, and safety-critical code stay with Claude.
Latency. Each delegation is a separate model call, typically 3 to 15 seconds through the CLI. For one moderately large file a direct read is often faster in wall-clock terms even though it costs more tokens.
The 90% figure. It is Spotify's mean over four bulk-read scenarios in a Java monorepo, not a whole-session number. Your mileage depends on how often you hit large files.
A naive claude -p --model haiku inherits the parent session's CLAUDE.md, skills, and MCP tool schemas. On a one-line test file that came to 62,549 input tokens before the worker read anything. shuntkit passes --setting-sources "", --tools "", --strict-mcp-config and a short custom system prompt, and runs the worker in an empty scratch directory, which brought the same call down to about 400 tokens.
upstream shunt |
shuntkit |
|
|---|---|---|
| Transport | Spotify Portal CLI, AiKA modes, Gemini 2.5 Flash | Claude Code CLI (default) or Anthropic API |
| Language | bash + jq |
Python, standard library only |
| Install | Plugin marketplace | pip/uv/pipx or plugin marketplace |
Read with offset and no limit |
allowed (documented as a known bypass) | blocked when the slice exceeds the threshold |
| Reassembling a file from many slices | possible | blocked by the per-session slice budget |
head -100 big.txt |
blocked | allowed (bounded slice, same as Read with limit) |
cat a.py b.py |
checks first file | sums all files |
| Line numbers in answers | none | verified (file:line) citations |
| Secrets | no check | name, directory and PEM-content guard |
code-write inputs |
reference file only | reference plus --context source files |
| Hook output | deprecated decision: block |
current hookSpecificOutput.permissionDecision |
| Delegation target | script only | script or native Haiku subagent |
| Usage tracking | stderr line | stderr line plus shuntkit stats |
| Tests | shell eval runner | pytest, upstream eval cases ported one-to-one, 140 tests |
See docs/design.md for how each mechanism works and where it stops, and docs/upstream.md for how this repo tracks upstream changes.
- Codex CLI as a worker. A
codex exectransport for Codex users without the Claude CLI, and for teams whose cheap model lives on the OpenAI side. Tracked in #5. - Shell forms beyond the current set.
awk 'NR>=A && NR<=B', heredoc andxargsreads recognised.
Ideas and pull requests are welcome; see CONTRIBUTING.md.
git clone https://github.com/melonkernel/shuntkit
cd shuntkit
uv sync
uv run pytest
uv run ruff check . && uv run ruff format --check .Editing skill or agent text: change src/shuntkit/skills.py, then run scripts/render-skills.py. A test fails if the checked-in skills/ or agents/ directories are stale.
The hook design, skill concepts, prompt shapes, and hook evaluation cases come from the shunt plugin in spotify/portal-ai-plugins, Copyright Spotify AB, Apache-2.0. Read the engineering post that started it. See NOTICE.
Three ideas were prompted by other independent ports of the shunt pattern, all MIT-licensed; shuntkit's implementations are its own. The per-session slice budget follows WinarG14/claude-shunt. Mechanically verified citations and the secret-path guard follow CameronCarlin/claude-gemini-context-shunt. The native-subagent delegation target follows teenaxta/openshunt.
shuntkit is not affiliated with Spotify or Anthropic.