shuntkit
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.
Why
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.
How it works
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, less, more, bat on large files |
Pipes, redirects, head/tail with a bounded count, grep |
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.
Install
You need Claude Code installed and logged in, and Python 3.10 or newer on macOS or Linux.
Option A: pip / uv (recommended)
uv tool install shuntkit # or: pipx install shuntkit / pip install shuntkit
shuntkit install # registers hooks, skills and the bulk-reader agent in ~/.claude
shuntkit doctor --probe # one tiny Haiku call to confirm it works
Restart Claude Code. Done.
Option B: Claude Code plugin
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.
Uninstall
shuntkit uninstall # removes exactly what 'install' added
Usage
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.py
Each 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.
Verified citations
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.
Secret guard
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.
Configuration
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). |
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. |
Using the Anthropic API instead of the CLI
pip install 'shuntkit[anthropic]'
export SHUNTKIT_TRANSPORT=anthropic
export ANTHROPIC_API_KEY=sk-ant-... # or: ant auth login
Useful if you want worker calls billed to an API key rather than your Claude Code plan, or if you run shuntkit outside Claude Code.
Subagent mode
export SHUNTKIT_DELEGATE=subagent
Blocked 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.
What to expect
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.
Why the nested CLI call is cheap
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.
Differences from upstream shunt
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.
Roadmap
- Codex CLI as a host. Codex's hook system mirrors Claude Code's (same
PreToolUseevent, same deny JSON), but Codex reads files through the shell rather than a Read tool, so the Bash hook needs to recognisesed -n,nlandrgcontext reads, and acodex execworker transport is needed for users without the Claude CLI. Tracked in #5. - Shell forms beyond
cat/head/tail.sed -n 'A,Bp'ranges charged to the slice budget; heredoc andxargsreads recognised. - Codex CLI as a worker. A
codextransport for teams whose cheap model lives on the OpenAI side.
Ideas and pull requests are welcome; see CONTRIBUTING.md.
Development
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.
Credits
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.
License
Metadata
Release files for shuntkit 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| shuntkit-0.1.0.tar.gz | 40.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| shuntkit-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 77.0 kB
Release files / shuntkit-0.1.0.tar.gz
| Download URL | shuntkit-0.1.0.tar.gz |
|---|---|
| Size | 40.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
553a02b97f96fc5f4b58346ae68c26155952e0ba6d7dd77ed6a63199d7d07bea
|
|
BLAKE2b-256 checksum How to use checksums |
eb122e92773525744aed14e8c1593d3e7e37b159310bd8c86cba17a5421b7fce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.
Transparency logRelease files / shuntkit-0.1.0-py3-none-any.whl
| Download URL | shuntkit-0.1.0-py3-none-any.whl |
|---|---|
| Size | 36.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fd66c4ef347578799f7ce19e4579d5f375346904684dfc2b648671cfa900bcf8
|
|
BLAKE2b-256 checksum How to use checksums |
165c5308f423cffc636bafe1f2f8baac5dae22c834d462bf85fe614e4830a641
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.
Transparency log