2026 年 9 月 3 日,Spotify Engineering 公開一套降低 Claude Code Token 主上下文用量的做法:Claude Code 想把大檔整份讀進上下文時,先用 Hook 擋下來,再交給另一個較便宜的模型摘要。文章標題寫著省下 90%,很誘人;但如果你把它理解成「帳單直接打 1 折」,就會從第一步算錯。
這篇 Claude Code Token 教學專為第一次碰 Hook 的讀者寫。我們會拆解 Spotify Portal/Shunt 的真正機制,針對 schema 相容性疑慮提供一份必須在實際版本驗收的專案級參考護欄,最後用同一組任務判斷它究竟省錢,還是只把成本和錯誤搬到另一個模型。
先說結論:可帶走的公式不是「Shunt=省 90%」,而是硬路由=Gate(擋下大讀取)+Worker(便宜模型做偵察)+Fallback(主模型查核關鍵行)。真正要最小化的是「完成一個正確任務的總成本」,不是某一個模型畫面上的 token。
Claude Code Token 分流到底在做什麼?
把主模型想成主治醫師。它應該負責診斷、取捨和最後決定,不必親自把整間病歷室逐頁搬過來。便宜模型比較像病歷助理:先找出相關檔案、符號和可核對原句,再讓主治醫師只看真正會影響判斷的頁面。
所以分流不是把 Claude 換掉,而是把「找哪裡」和「判斷怎麼改」拆開。這也延續了 AI Agent Harness 的核心觀念:模型負責推理,Harness 負責決定它能看什麼、何時用工具、失敗後往哪走。

先拆穿「省 90%」:省的是哪一段?
Spotify 原文的數字來自作者描述、但未公開的 Java monorepo(把多個專案放在同一個版本庫)。固定版本 README 的公開表格列出三個 bulk-read 情境,分別估算出 82%、94%、94% 的 Claude 可見上下文縮減,算術平均約 90%。計算程式用的是字元數 ÷ 4近似 token;比較的是「原始檔案全文」和「worker 回傳摘要」,不是供應商帳單。
截至 2026 年 9 月 6 日,Spotify 文章、固定在 commit 3c24ca3 的 README 與 benchmark 腳本沒有發布那套 162K 行 Java 原始庫、worker 原始回應、重複試驗、變異或品質評分,也沒有把 worker 的輸入/輸出、Portal 開銷、重試、主模型後續重讀和人工返工加回去。因此最準確的讀法是:這是三個 bulk-read 案例中,Claude 主上下文約少九成的作者估算;不是總 token、總成本或任務品質的通用 benchmark。

這並不代表方法沒價值。它只是把問題改寫成更誠實的一句話:
完成成本=主模型成本+worker 成本+Portal/基礎設施成本+等待時間+錯誤重做。
如果你只看第一項,幾乎一定會高估收益。想先盤點不加 router 的基本功,可讀 Claude Code 省 Token 指南;要留完整用量憑證,可搭配 Tare Claude Code Token 稽核教學;想理解介面本身帶來的額外負擔,則看 MCP vs CLI Harness Tax A/B Test。
Spotify Shunt 的 3 層:提醒為什麼不夠?
① Gate:「先把整份大檔攔下來」
把「請節省 token」寫進 CLAUDE.md 只是建議,Agent 仍可能忘記。Shunt 改用 PreToolUse Hook,在 Read 真正執行前檢查檔案大小;目前公開 v0.2.0 的預設門檻是超過 350 行。有 offset 或 limit 的定點讀取則放行,因為主模型已知道要看哪一段。
② Worker:「大檔送去另一條推理管線」
Shunt 的 bulk-read 腳本把選定檔案包成有路徑邊界的內容,透過 Portal CLI 呼叫 AiKA mode。Mode 決定用哪個模型、提示詞與工具;主 Claude session 不收到 raw corpus,而是收到 worker answer,另帶 Shunt status/tool metadata。換句話說,原始內容仍被另一個模型處理,只是不再整份塞進主模型會反覆攜帶的上下文。
③ Skill+Fallback:「把摘要變成可查證的地圖」
Hook 只能擋,不能把 Read 變成另一種工具。安裝官方 plugin 後,實際 Skill 指令是帶 namespace 的 /shunt:bulk-reader;它會呼叫 worker 做文字偵察。把它回報的檔名、符號與候選位置當線索,不要直接當成可編輯的可靠行號。若摘要低信心、缺少證據,或任務是 debugging、安全、併發與架構取捨,就回到主模型做有限範圍的精讀。
先選 Worker 路徑:官方 Shunt 或自備 Adapter
先不要啟用阻擋 Hook。Hook 只能拒絕這次 Read,不會憑空生出替代模型;先把 worker 路徑跑通,再加 Gate,才不會把 Agent 卡在「不能讀、也無處可去」。
走官方路徑時,Shunt v0.2.0 不是獨立的免費本機 router。你需要可登入的 Spotify Portal、已啟用 AiKA、可用的 AiKA mode(Portal 裡綁定模型、提示詞與工具的任務設定),以及已設定 provider 的 AI Gateway。provider 是實際執行推理的模型服務;gateway 是統一轉送、認證與治理請求的入口。所謂「不用 API key」只代表終端使用者不用自己貼 key,管理者仍要設定供應商憑證或核准的本機 endpoint。
條件具備後,依官方 repo 固定版本說明安裝:
claude plugin marketplace add spotify/portal-ai-plugins
claude plugin install portal@portal
claude plugin install shunt@portal
開新 session 後跑 /portal:setup,再用 /hooks 確認 Hook。查詢你的 workspace 是否真的有 bulk-reader mode;Portal 的「public mode」是該 workspace 內的可見範圍,不代表每個客戶環境自動有同名 mode。先用一份無敏感資料的測試檔確認 worker 能回應,再往下做。
兩條路徑都需要 jq。先跑 command -v jq && jq --version;找不到時,macOS 可用 brew install jq,Debian/Ubuntu 可用 sudo apt-get install jq,再重新驗證。若公司限制套件安裝,先請管理員提供核准版本。
若不使用 Portal,你要先提供一個組織核准、可以接收檔案並回傳固定 JSON 合約的 worker CLI/Skill,完成登入、權限、timeout 與無敏感資料的 smoke test。本文的 provider-neutral 內容定義它的輸出與故障規則,不假裝 Hook 本身就是 worker。
還有一個版本陷阱:Spotify 9 月文章仍寫 Portal 有 30 秒上限,而且連到作者較舊的 fork;但官方 repo 的 PR #7 已在 8 月 14 日合併,v0.2.0 transport 預設 SHUNT_TIMEOUT_SECONDS=180 並傳入 CLI timeout 參數。不要把文章中的 30 秒硬寫進設定;以實際安裝版本的 repo 與 CLI help 為準,並把超時記成 A/B 的失敗事件。
動手做:5 步建立 exit-2 Read 阻擋護欄
截至 2026 年 9 月 6 日,Shunt v0.2.0 的 Hook 仍輸出舊式頂層 decision。Claude Code 最新 Hook 文件把目前格式定義為 hookSpecificOutput.permissionDecision;舊格式有相容映射,但一名 reporter 在自己的環境與 main 分支上透過 Issue #10 回報 fail-open,尚無維護者確認或版本矩陣。下面用官方定義的 exit 2,針對 schema 相容性疑慮加一層專案護欄,不修改 plugin cache。
這仍不是絕對 fail-closed。官方文件說明:command Hook 只要成功跑到 exit 2 就會擋下 PreToolUse;但腳本找不到、無執行權限或逾時時,工具會回到一般 permission flow。所以下面的參考實作必須在你實際使用的 Claude Code 版本做端到端驗收,不能只測 shell 回傳碼。
第 1 步:先定義你真正要擋的範圍
痛點:行數抓不到單行 200 KB 的 minified JS/JSON,而「有 limit 就放行」也可能被超大數字繞過。解法:同時檢查行數、bytes 與定點讀取上限。先用 ROUTER_MIN_LINES=350、ROUTER_MAX_BYTES=50000、ROUTER_MAX_DIRECT_LINES=200 當實驗起點,再用 A/B 結果調整;它們是起始旋鈕,不是程式碼難度的自然定律。
第 2 步:建立阻擋腳本
建立 .claude/hooks/guard-large-read.sh。以下版本只把「正數且不超過 ROUTER_MAX_DIRECT_LINES 的 limit」視為有限讀取;只要腳本成功啟動,解析錯誤或缺少 jq 時就以 exit 2 阻擋,不會由腳本回傳放行。
#!/usr/bin/env bash
set -u
set -o pipefail
if ! command -v jq >/dev/null 2>&1; then
echo "ROUTE_LARGE_READ: jq is unavailable, so the routing gate cannot inspect this Read request." >&2
exit 2
fi
input=$(</dev/stdin)
if ! printf '%s' "$input" | jq -e . >/dev/null 2>&1; then
echo "ROUTE_LARGE_READ: malformed hook input; refusing to fail open." >&2
exit 2
fi
file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
offset=$(printf '%s' "$input" | jq -r '.tool_input.offset // empty')
limit=$(printf '%s' "$input" | jq -r '.tool_input.limit // empty')
# Let Claude Code itself report an absent or unreadable path.
if [ -z "$file_path" ] || [ ! -f "$file_path" ] || [ ! -r "$file_path" ]; then
exit 0
fi
# Route only an explicit text/code allowlist. Images, PDFs, binaries and
# extensionless secrets stay on Claude Code's normal Read path.
case "$file_path" in
*.c|*.cc|*.cpp|*.cs|*.css|*.go|*.h|*.hpp|*.html|*.java|*.js|*.jsx|*.json|*.kt|*.kts|*.lock|*.md|*.php|*.py|*.rb|*.rs|*.sh|*.sql|*.swift|*.toml|*.ts|*.tsx|*.txt|*.xml|*.yaml|*.yml|*/Makefile|*/Dockerfile|*/Containerfile) ;;
*) exit 0 ;;
esac
min_lines=${ROUTER_MIN_LINES:-350}
max_bytes=${ROUTER_MAX_BYTES:-50000}
max_direct_lines=${ROUTER_MAX_DIRECT_LINES:-200}
case "$min_lines" in ''|*[!0-9]*) min_lines=350 ;; esac
case "$max_bytes" in ''|*[!0-9]*) max_bytes=50000 ;; esac
case "$max_direct_lines" in ''|*[!0-9]*) max_direct_lines=200 ;; esac
# A targeted Read is allowed only when both its line count and the bytes in
# that exact slice stay bounded. This catches one-line minified files.
if [[ "$limit" =~ ^[0-9]+$ ]] && [ "${#limit}" -le 9 ] && [ "$limit" -gt 0 ] && [ "$limit" -le "$max_direct_lines" ]; then
start=1
if [ -n "$offset" ]; then
if [[ "$offset" =~ ^[0-9]+$ ]] && [ "${#offset}" -le 9 ] && [ "$offset" -gt 0 ]; then
start=$offset
else
start=0
fi
fi
if [ "$start" -gt 0 ]; then
end=$((start + limit - 1))
if ! slice_bytes=$(sed -n "${start},${end}p" "$file_path" | wc -c); then
echo "ROUTE_LARGE_READ: slice measurement failed; refusing to return allow." >&2
exit 2
fi
slice_bytes=${slice_bytes//[[:space:]]/}
if [ "$slice_bytes" -le "$max_bytes" ]; then
exit 0
fi
fi
fi
if ! lines=$(awk 'END { print NR }' "$file_path" 2>/dev/null); then
echo "ROUTE_LARGE_READ: line count failed; refusing to fail open." >&2
exit 2
fi
if ! bytes=$(wc -c < "$file_path" 2>/dev/null); then
echo "ROUTE_LARGE_READ: byte count failed; refusing to fail open." >&2
exit 2
fi
lines=${lines//[[:space:]]/}
bytes=${bytes//[[:space:]]/}
if [ "$lines" -gt "$min_lines" ] || [ "$bytes" -gt "$max_bytes" ]; then
printf 'ROUTE_LARGE_READ: blocked full read of %s (%s lines, %s bytes). Run /shunt:bulk-reader or the approved adapter first; require file/symbol/exact-quote evidence. For debugging, architecture, security, or an edit, reissue a bounded Read with limit and verify the exact section.\n' \
"$file_path" "$lines" "$bytes" >&2
exit 2
fi
exit 0
這份 allowlist 刻意只收常見文字與程式碼副檔名;PNG、PDF、binary 與可能是 secret 的無副檔名檔案都交回 Claude Code 原本的 Read 流程,不送進文字 worker。要增加新型別時,先用無敏感資料驗證 worker 能正確處理,再明確加入清單。
接著執行 chmod +x .claude/hooks/guard-large-read.sh。這裡選 exit 2 是關鍵:Claude 官方文件把它定義為 PreToolUse 的阻擋結果,stderr 內容會回到 Agent,成為下一步改走 bulk-reader 的提示。
第 3 步:把 Hook 接到專案設定
在 .claude/settings.json 合併下面的 PreToolUse 設定;若檔案本來已有 hooks,不要整段覆蓋。
{
"env": {
"ROUTER_MIN_LINES": "350",
"ROUTER_MAX_BYTES": "50000",
"ROUTER_MAX_DIRECT_LINES": "200"
},
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-large-read.sh"
}
]
}
]
}
}
第 4 步:替自備 Worker 設計供應商中立合約
先分清楚:上面的 shell 是本篇可執行、可單元測試的 Gate;下面是自備 adapter 的設計合約,不是 Shunt 內建功能,也不是一個已完成的 provider client。官方 /shunt:bulk-reader 目前回傳的是 prose,不會自動符合這份 JSON。只有當你已經有核准的 worker adapter,才要求它驗證並輸出以下 schema。
痛點:換模型後,摘要格式跟著漂;Shunt 傳給 worker 的原始內容也沒有可靠行號,不能把模型猜的範圍當證據。解法:不管後端是 Portal AiKA、公司 gateway 或核准的本機模型,都要求同一份輸出:summary、evidence[path,symbol,exact_quote,candidate_start_line]、confidence、needs_direct_read。其中行號只是候選,worker 只「指路」,主模型才「判案」。
{
"summary": "一句話回答問題",
"evidence": [
{
"path": "src/service.ts",
"symbol": "refreshSession",
"exact_quote": "const next = await rotateToken(current)",
"candidate_start_line": 120
}
],
"confidence": "medium",
"needs_direct_read": true
}
confidence 只允許 low、medium、high。收到回應後,先把 worker 給的 path 當成不可信輸入:解析 canonical path,確認它仍在核准的 repo 與本次請求檔案集合內,才可搜尋。接著在本機用 rg -n -F -- '完整原句' src/service.ts 解析 exact_quote;只有唯一命中時才把真正行號換算成有限的 Read(file_path, offset, limit)。若仍有可信 symbol,就在本機搜尋後由主模型查核;若 worker 非零退出且沒有 evidence/symbol,先用本機 Grep/Glob 縮小候選集,仍無法建立證據就 visible stop。這就是 provider-neutral fallback:更換後端不必重寫 Gate,故障時也不退回「整份吞進主上下文」。想進一步理解這種故障切換,可延伸看 LLM Chaos Drill 多模型備援。
第 5 步:測 8 個邊界,不要只看「Hook 有出字」
- 350 行完整讀取應放行;有沒有結尾換行的 351 行檔都應阻擋。
- 351 行、
limit=80應放行。 - 351 行、
limit=1000000應阻擋。 - 超過 byte 門檻的單行
.js應阻擋。 - 大型 PNG/PDF/binary 不應被誤送到文字 worker。
- 不存在的檔案交回 Claude Code 顯示原始錯誤。
- 壞 JSON 或缺少
jq時,成功啟動的腳本必須回傳exit 2。 - 在 Claude Code 內用
/hooks確認來源,再實際要求讀大檔,驗收工具真的被擋;腳本路徑錯誤與 timeout 也要各測一次。
注意:上面只針對內建 Read 建立邊界,而且要用最後一項端到端測試確認你的 Claude Code 版本真的執行了它。Claude 仍可透過 Bash 的 cat、sed、直譯器或其他工具讀檔;官方 Shunt 的 Bash parser 也有 pipe、複合指令、空白路徑與 minified file 等繞過面。需要組織級強制時,應把 Bash permission、sandbox、敏感路徑 deny policy 和可觀測性一起設計;不要把一段 regex 宣稱成完整資料防漏。
Claude Code Token A/B:同任務怎麼比才公平?
不要拿昨天的修 bug 和今天的寫文件相比。選 6–10 個可重複任務,讓 A、B 各自從相同 commit 的乾淨 worktree 起跑,固定 Claude 模型、提示詞、工具權限與測試命令,事先寫好 gold answer/測試 rubric。A 組關閉 router,B 組打開 router;每題都用新 session 隔離對話歷史,但 prompt cache 仍可能存在,因此要記 cache read/creation tokens,讓兩組同樣預熱或交錯、隨機化執行順序。每題至少重複 3 次,再比較中位數與失敗分布。
- 主模型:用官方文件說明的
/usage記 input、output、cache 與 session 資訊;API 用戶再以 Console 帳單核對。 - worker:Shunt 本身不會在結果裡提供實際 provider tokens、費用、重試與 resolved mode id;要從 AI Gateway、provider 帳單或核准的 trace 取得。拿不到就標成 unknown,不能下「省錢」結論。
- 時間:從送出同一提示到測試完成的 wall-clock,不只看模型 latency。
- 品質:測試通過率、問題清單召回率、錯誤行號、review 發現的缺陷。
- 返工:主模型重新讀全文的次數,以及人工修正分鐘數。

建議在開跑前寫 stop rules:只要 B 組出現重大漏讀、測試通過率下降、總 provider 成本沒有改善,或中位完成時間超過團隊可接受上限,就停止擴大;若只有某類任務有效,就把分流範圍收窄到那一類。完整的同 repo 實驗設計,可接著讀 Claude Code Token A/B Test 教學。
哪些任務該分流?哪些要留給主模型?
適合先交給 worker:找出某個介面有哪些實作、列出呼叫鏈候選、摘要多份設定檔、替文件建立索引、回報可能相關的檔案、符號與可核對原句。共同點是答案可以先在本機解析位置,再被主模型用少量定點讀取快速驗證。
直接留給主模型:併發 bug、安全邊界、架構取捨、資料遷移、會寫回檔案的精確編輯,以及任何「漏一個細節就可能出事」的判斷。Spotify 作者自己記錄到 worker 漏掉 subtle thread-safety bug;這個案例正好說明便宜偵察不是便宜裁判。
如果你的問題其實是 Agent 大量生成子代理,而不是讀大檔,機制不同:請用 Claude Code Ultracode 四道派工煞車。前者管的是 I/O 路由,後者管的是 fan-out 爆量,兩者可以同時存在,但不要共用同一個 KPI。
資料與故障:最容易被省略的 4 個坑
- 原始碼會去哪裡:Portal 路徑會把所選檔案送到 Portal backend 與其設定的模型 provider,目前 Shunt transport 還會把完整 payload 放進 CLI argv。路徑邊界標籤不是安全 sandbox;先驗證送出的 canonical paths,核對 repo 分級、provider、trace/retention 設定。處理不可信 repo 時選擇不帶外部工具的專用 mode;secret、客戶資料與受管制程式碼不要進未核准的 mode。
- 失敗不等於自動回主模型:CLI 未登入、mode 不存在、payload 過大、timeout 或回應壞掉時,公開 Shunt 腳本會失敗退出。你的 Skill 必須明寫「縮小批次 → 定點直讀 → 停止」,不能只期待 Agent 自己猜。
- 摘要可能製造 false negative:worker 沒指出的檔案不代表不相關。對安全、debug、併發與資料一致性問題,主模型要用搜尋與定點讀取做第二條證據鏈。
- 省配額不一定省現金:Pro/Max 訂閱、Claude API、Portal 訂閱和 worker provider 的計價邏輯不同。把所有帳單與固定成本放進同一張實驗表,再決定是否推廣。
常見問題(FAQ)
Q1:Spotify Shunt 真的能讓 Claude Code Token 省 90% 嗎?
不能直接這樣說。90% 是三個 bulk-read 情境中「Claude 可見上下文」的近似平均,不是完整任務、現金成本或品質保證。你的結果要用同任務 A/B 才知道。
Q2:一定要買 Spotify Portal 嗎?
使用官方 Shunt worker 路徑時,需要可登入的 Portal/AiKA 環境。但 Gate+Worker+Fallback 這個架構不綁 Portal;你可以接組織核准的 gateway 或本機 worker,只要維持相同證據合約。
Q3:350 行是最佳門檻嗎?
不一定。350 是 Shunt v0.2.0 的預設值,不會衡量語意難度。從它起跑,再依檔案型態、摘要品質、延遲與重讀率調整;同時設 byte 門檻,才不會漏掉巨大的單行檔。
Q4:Hook 可以直接把 Read 換成便宜模型嗎?
不是同一個動作。PreToolUse 先阻擋 Read,再把原因回給 Agent;Skill 或腳本才負責呼叫 worker。把這兩層分開,才容易替換 provider 與測試失敗路徑。
Q5:便宜模型可以直接改檔嗎?
這篇做法不建議。worker 先輸出證據地圖;主模型定點讀取、編輯並跑測試。這會少省一些上下文,卻保留可追溯的最後一道品質關。
Q6:我用 Pro/Max,還需要做嗎?
看瓶頸。若你常被長上下文拖慢或撞到方案用量,分流可能有價值;若主要問題是正確率與等待時間,額外 worker 反而可能不划算。用 /usage 和實際完成時間決定。
Q7:worker 壞掉時,要自動讀完整大檔嗎?
不要無條件整份 fallback。先縮小檔案集合,再依 evidence 做有限 offset+limit 讀取;仍無法建立證據,就把任務停在可見錯誤,而不是悄悄燒回原本成本。
Q8:這套 Hook 能保證任何大檔都不進主上下文嗎?
不能保證。本文最小版只針對符合條件的內建 Read 呼叫設計;你仍要在實際 Claude Code 版本做端到端阻擋測試。Bash、其他工具與管理員政策是另一層邊界;組織級控制還要加 managed permissions、sandbox、路徑政策與持續監測。
給新手的 5 個重點
- 把 90% 讀成特定 bulk-read 的主上下文縮減,不是帳單折扣。
- 用 Hook 做 deterministic Gate,不只在提示詞裡拜託 Agent 節省。
- worker 只提供檔案、符號與可核對原句;真正行號由本機
rg唯一命中解析,主模型再判斷。 - 同時記主模型、worker、時間、測試與返工,才有完成成本。
- 先在一個 repo、少量可重複任務試跑;過 stop rules 才擴大。
如果你想把這套路由與完整 Agent 執行層一起學,先讀 最小 AI Agent Harness 實作,再到 AlphaLab 線上課程把評測、系統設計與可靠性補齊;其他入門與實作題目則集中在 AlphaLab AI 專區。
接著閱讀
左右滑動查看更多推薦
結語:先把「省 token」改成「省正確完成的成本」
Shunt 最值得學的不是某個 90% 數字,而是把模型分工從一句軟性提醒,變成可觀測、可失敗、可回退的執行路徑。回到開頭的錨點:Gate 擋大讀取,Worker 做低風險偵察,Fallback 讓主模型查核關鍵證據。
你的下一步很小:先用無敏感資料讓 worker smoke test 通過,再挑一個 repo、六個固定任務啟用 Read Gate,記下 A/B 的總成本、時間、測試與返工。若 B 組真的用更少資源完成同樣正確的工作,再把同一組設定部署到第二個 repo;這才是能複製的 Claude Code Token 省法。






