用 Claude Code 寫了一陣子,許多人都會被額度頁面嚇一跳——明明只跑了幾個任務,session 額度就掉了一大半。其實多數時候模型本身沒有特別聒噪,問題出在 prompt cache 沒命中:每一輪對話如果無法重複利用前面的快取,就等於把整段 system prompt、CLAUDE.md、過往對話全部重新計費送進去一次。Anthropic 的計費也明確區分了三種 token 來源(fresh input、cache write、cache read),三者單價差距很大,有 cache 的部分只需要 10% 的 Token 費用,是否吃到 cache 直接決定一整段對話的成本。

這篇文章會先給一份節省 Token 的要點清單,方便忙的時候快速翻閱對照;接著再從 Claude Code 怎麼計費、prompt cache 怎麼運作講起,聊什麼操作會無聲無息把快取打破——包含很多人會誤會的「點兩下 ESC 」到底會不會讓快取失效。最後分享我自己用的一份 statusline 腳本,把 hit rate、快取剩餘時間、context 用量都印在狀態列,再搭配 refreshInterval 每 10 秒自動刷新,避免在不知情的狀況下持續燒額度。

節省 Token 的要點速覽

名詞須知

後面要點會反覆出現幾個名詞,先用一張表把意思固定下來,避免讀到一半卡住。技術細節在後面章節會展開,這裡只給最關鍵的一句話定義:

名詞意思
上下文(context window)模型一次能看到的所有內容上限,包含 system prompt、歷史對話、工具回傳。Claude Code 在狀態列會顯示為 ctx 百分比
system prompt對話開頭由系統送進去的指令集,告訴模型扮演什麼角色、可以用哪些工具。Claude Code 自己組好的,使用者不直接寫
CLAUDE.md專案根目錄或全域 ~/.claude/CLAUDE.md 的常駐指令檔,每輪請求都會被塞進 system prompt 後面
Claude Code Skills放在 .claude/skills/~/.claude/skills/ 的 markdown 工作流,預設只載入 frontmatter 描述、被觸發時才注入主體
Subagent跑在獨立上下文裡的 agent,主對話只會收到它的最終結論。內建有 Explore、general-purpose、Plan
Prompt cacheAnthropic 在 API 層提供的快取機制,把 request 從頭往後切片快取,下一輪同樣的前綴可以重用避免重算
Cache hit / read本輪命中快取、被讀回來的 token,單價只有標準輸入價的 10%
Cache write本輪寫入快取的 token,單價比標準輸入貴 25%(5 分鐘 TTL)或貴一倍(1 小時 TTL)
Fresh input本輪沒命中快取也沒寫入快取的新內容(如使用者剛輸入的訊息、工具最新回傳),用標準輸入價
TTL(time to live)快取存活時間。Pro 方案約 5 分鐘、Max 方案約 1 小時,超過就清掉
Auto-compact上下文用量逼近上限時 Claude Code 自動把舊對話濃縮成摘要,會徹底清空整段快取
/clearClaude Code 指令,把對話歷史清掉重新開始;同時也會丟掉所有原本的快取

要點清單

下面這幾條是平常用 Claude Code 最容易實踐、效果也最直接的省 token 撇步。多數圍繞著「不要動到對話前綴」這件事,背後跟 prompt cache 的運作有關(後面章節會展開),這裡先當清單翻閱:

  • CLAUDE.md 寫精簡,能搬到 Skill 就搬:CLAUDE.md 每個字都會塞進每一輪請求,編輯一次就會打斷後續的快取重用。把「特定情境才需要的工作流」改寫成 Claude Code Skills 延遲載入,沒被觸發時完全不耗 token。
  • 完成大型任務後主動 /clear:跑完一個大功能或長除錯後,上下文(ctx)通常已經堆了一堆無關的檔案內容、grep 結果、試錯紀錄,再繼續疊新任務只會讓 auto-compact 提早觸發。任務一段落就 /clear,雖然下一輪要付一次基底重建費(system prompt + CLAUDE.md + skills 重新寫入快取),但換到後面整段對話跑在乾淨環境裡值得,也避免被舊脈絡帶偏。
  • 小任務之間別亂 /clear:上下文還在個位數時清掉反而不划算——基底重建費比省下的還多。同一個主題下接連問幾個小問題、或修同一支檔案的不同地方,繼續疊上去吃 cache read 比較便宜。判斷準則:如果新問題還會用到剛才的脈絡,就別清;明顯換主題且 ctx 已爬到一定水位,才考慮清。
  • 用 Plan Mode 一次想清楚再動手:Shift+Tab 切到 Plan mode 會讓 Claude 先把計畫整段提出來等確認再執行。方向錯誤時在 plan 階段否決掉只損失一輪 output token,遠比寫完才打掉重練便宜。
  • 研究型任務丟給 Subagent 隔離上下文:跨檔搜尋、長文件閱讀這類「要讀很多但結論很短」的任務丟給 subagent 處理,獨立上下文讀完一堆檔案,最後只把結論回傳,主對話的上下文不會被污染。具體操作見後面〈用 Subagent 處理研究型任務〉一節。
  • 讀檔用 offsetlimit,不要整檔吐出來:Claude Code 內建的 Read 工具支援指定行數範圍。只關心某段函式或設定時,事先指定範圍會比讀整檔節省可觀的 token,搜尋結果用 grep -A-B 抓上下文也是同樣道理。
  • 不要為了比較頻繁切模型:用 /model 在 Opus/Sonnet/Haiku 之間切換,每切一次都會把整段對話前綴重建一次。任務做到一半覺得卡住時換模型的成本比想像中高。
  • 盯著狀態列上的 ctxhit,別讓 auto-compact 偷襲:上下文用量逼近上限時 Claude Code 會自動 compact,一次清空整段快取。把 statusline 設好(後面有完整範例),看到 ctx 欄位變紅就主動收尾,比讓系統幫你決定划算很多。
  • 長時間離開前先評估:Pro 方案 prompt cache 5 分鐘過期、Max 方案 1 小時,超過 TTL 回來第一句一定 miss。如果只是離開 10 分鐘左右,max 方案完全不用擔心;如果預期離開更久就不用刻意保留快取。
  • 短暫離開可用 /loop 養快取:Pro 方案如果只是去倒杯咖啡、開個短會 5–15 分鐘就回來,可以開另一個視窗下 /loop 4m say hi,每 4 分鐘戳一次「hi」把 TTL 往後續,回來時 cache 還在。要小心這個動作每輪都會吃 session/weekly 額度(每次 cache read + 一輪 hi/output 計費),所以離開超過 30 分鐘以上反而比直接吃一次重建費更貴,記得回來時先 /loop --stop 收掉。

上面這些撇步「為什麼有效」的細節,會在後面講完計費邏輯與 prompt cache 機制後變得更清楚。先進入計費怎麼算的部分。



Sponsored Links

Claude Code 的 Token 怎麼算

Claude API 把每一次 request 的 token 拆成四個欄位回報,這四個欄位也是 Claude Code statusline 拿得到的數值:

欄位意思計費特性
input tokens本輪新送進去、沒被快取的內容標準輸入價
cache creation本輪寫入快取的 token(cache write)比輸入貴 25%(5m TTL)或貴一倍(1h TTL)
cache read本輪命中快取、被讀回來的 token只要輸入價的 10%
output tokens模型回應產生的 token輸出價(最貴)

這張表的關鍵在最後兩欄:cache read 的單價只有標準輸入的十分之一,所以一段對話的成本能不能壓低,幾乎就看 cache read 占整體輸入的比例有多高。我們把這個比例稱為 cache hit rate,計算方式是 cache_read / (cache_read + cache_write + input)。在一輪正常的延續對話裡,hit rate 應該要落在 90% 以上;如果掉到 70% 以下就代表這輪有大量內容被當成新輸入重算,背後通常是快取被破壞了。

Prompt Cache 的運作原理

Anthropic 的 prompt cache 走的是前綴比對(prefix match)。每次 request 送進去之後,伺服器會把整段 prompt 從頭往後切成多個區段,每個區段算一個 hash key 暫存在快取裡。下一次 request 進來時,從第一個 token 開始往後比對,能對得上的部分就直接用快取版本,從第一個對不上的位置往後就全部重算並寫入新快取。

這個機制有兩個推論很重要。第一,快取是「從頭比對」而不是「整體比對」,所以前綴只要動到一個字,後面整段就全部失效。第二,快取有 TTL:API 預設 5 分鐘版本,每多一筆 request 命中就會把過期時間往後續,沒人來戳的話過了 5 分鐘整段消失。Claude 也提供 1 小時 TTL 的擴充版本(價格上 cache write 收費翻倍、但 cache read 一樣便宜),目前觀察 Claude Code 在 Max 方案下會自動使用 1 小時 TTL,Pro 方案則是 5 分鐘。

這也解釋了為什麼長時間放著不動再回來,第一句話會特別貴——因為原本的快取已經過期被清掉,這輪等於把整個 system prompt、CLAUDE.md、所有歷史對話重新寫進新快取。也就是 statusline 上會看到 cache write 飆高、cache read 變成 0、hit rate 直接歸零的狀況。

Prompt cache 前綴比對示意圖:情境 A 正常延續對話前綴完全一致,整段歷史維持 cache read;情境 B 編輯 CLAUDE.md 之後從修改點往後 skills、對話歷史全部變成 cache write,hit rate 大幅下滑
前綴比對的兩種情境——前綴一致時整段都吃 cache read,一旦中間某一段被改動(例如 CLAUDE.md),從分歧點往後全部要重新寫入快取。

Context 越大,每輪對話越貴

很多人以為「有 cache 就不用怕對話變長」,這個誤會其實不小。Prompt cache 確實把已快取部分的單價打到 10%,但快取的內容還是要在每一輪請求裡整段重新送進去讓模型看見——只是用 cache read 的便宜價結帳,並不是「同樣的內容只算一次」。換句話說,對話跑得越久、上下文堆得越高,每一輪的輸入帳單也會跟著線性成長,差別只在於成長的斜率比沒 cache 時平緩許多。

具體算一下就很清楚。以 Opus 標準輸入 $15/M、cache read $1.5/M 為例,假設 cache hit rate 都維持在 95%:上下文還在 10k tokens 時,每輪輸入成本約 $0.08;爬到 100k 時來到 $0.27;接近 200k 時就漲到 $0.49。同樣一句「我繼續修這支函式」的對話,差別只在背後拖了多少歷史,這一輪的輸入帳單就能差到六倍以上。如果完全沒有 cache(例如剛經歷 auto-compact 或 /clear 後第一輪),同樣 200k 上下文每輪會直接跳到 $3.05,差距更誇張。

上下文用量影響每輪對話輸入成本:以 Opus 為例,cache hit 95% 時 10k context 每輪 $0.08、200k 每輪 $0.49;無 cache 全部 fresh input 則從 $0.20 一路飆到 $3.05,200k 時兩者差約 6 倍
同樣的 200k 上下文,cache hit 95% 每輪只要 $0.49,沒有 cache 就要 $3.05──cache 把成長斜率壓平到原本的六分之一,但帳單仍然會隨上下文線性成長。

所以與其等到 auto-compact 觸發才被動收場,平常就盯著 statusline 上的 ctx 百分比、適時用 /clear 或拆 subagent 把上下文壓低,才是長期省成本的關鍵。後面的 statusline 章節會把這個監控做法補完整。

什麼操作會破壞快取

因為快取走前綴比對,只要對話「從中間任何一處」開始與先前不同,後面就全部失效。常見的破壞情境整理如下:

改動 system prompt 來源檔

CLAUDE.md(不管專案的還是全域 ~/.claude/CLAUDE.md)一旦被編輯儲存,下一輪對話組起來的 prompt 前段就跟前一輪不同,整個 cache 會從那個位置往後全部失效。新增、移除、啟用、停用一個 skill 也是同樣效果——skill 的 frontmatter 列表會被注入到系統提示裡,列表變了前綴就斷掉。切換 output style 也會落在這個段落,等於每改一次設定就要付一次重建快取的錢。

切換模型

Opus、Sonnet、Haiku 之間互換時,不只是模型換掉,連對應的 system prompt 也會被替換成適合該模型的版本,等於整段前綴重洗。常見的反模式是任務做到一半覺得卡住,連續用 /model 切來切去比較,每切一次就燒一次完整快取重建費。

Auto-compact 與 /compact

當上下文用量逼近上限,Claude Code 會自動觸發 compact,把過往對話濃縮成摘要再繼續。compact 之後的對話前綴是「全新的摘要文字」而不是「原本的歷史訊息」,所以 compact 會徹底清空整個快取。手動 /compact 也是一樣效果。這是為什麼 statusline 上監控上下文百分比有意義——超過某個門檻就該主動收尾或 /clear,避免讓 auto-compact 在不知情的狀況下重建一切。

/clear 與開新 session

/clear 直接把對話歷史清掉重新來,舊快取會被孤立在那邊等 TTL 過期自然消失。從成本角度看,/clear 跟 compact 都會讓下一輪付一次完整重建費,但 /clear 的好處是「乾淨」:模型不會被先前無關的脈絡帶歪,同樣一筆 cache write 換到的是更聚焦的工作環境。如果手上的任務跟前面對話完全無關,/clear 永遠比繼續疊上去划算。

TTL 過期

前面提到 Pro 5 分鐘、Max 1 小時。中間長時間沒互動再回來時,第一輪一定 miss。如果預期會離開電腦一陣子又不想付重建費,可以在離開前刻意送一個小訊息延長 TTL,或是接受重建費直接離開。實務上 Max 的 1 小時 TTL 已經寬鬆很多,平常工作節奏下幾乎不會自然過期。

點兩下 ESC 會不會破壞快取?

這題很多人有疑問。Claude Code 在輸入框按兩下 ESC 會跳出歷史紀錄選單,讓使用者把對話倒帶到前面某一輪重新開始。從快取的角度看,倒帶本身不會讓被丟掉的那段 cache「立刻失效」——倒帶後的前綴跟原本的對話前綴完全一致,所以倒帶之後重新發出的第一輪訊息,前面那段 cache 會 hit;分歧發生在「倒帶之後輸入的新內容」與「原本被丟掉的那段內容」不同的地方,從那裡之後全部要重算。

換句話說,點兩下 ESC 倒帶的損失等於「丟掉的那段對話的 cache write 成本白付了」,但前面共用的快取還能繼續用。實務上這比 /clear 划算很多,特別適合「方向走偏想換條路試試」的情境。要注意的是被丟掉的那段 cache 雖然在伺服器上還沒到期,但因為新對話不會再走那條路徑,所以那段快取就只是靜靜地等 TTL 過期消失,再也不會被用到。

點兩下 ESC 倒帶對快取影響示意圖:主線是共用前綴 → 訊息 X → 點兩下 ESC,上方紫色虛線弧線從 ESC 繞回分歧點代表倒帶動作,下方藍色分支從分歧點岔出到訊息 X' 成為新方向
原始路徑走到 ESC 後倒帶回分歧點,訊息 X 那條路停留不再走;新方向從分歧點岔下去成為實際使用的分支。

Fork session 也是同樣道理

Claude Code 用 --fork-session(搭配 --resume--continue)會把目前對話複製成一個新的 session ID 繼續走,原本的 session 完整保留。從快取角度看,跟點兩下 ESC 是同一個道理:Anthropic 的 prompt cache 是用「prompt 內容前綴的 hash」當 key,不綁 session ID。所以 fork 出去的新 session 在分歧點之前的內容跟母 session 一字不差,那段照樣命中 cache read;只有分歧之後新輸入的內容才會走 cache write/fresh input。

跟 ESC 倒帶的差別只在「原路徑要不要保留」:ESC 倒帶把原本那段路覆蓋掉再也回不去;fork 則是兩條分支並存,可以在 session 選單裡隨時切回母 session 繼續原本那條路。實務上想做「同一個前提,分頭試兩種解法」就用 fork,已經確定方向走偏不會回頭就用 ESC——兩者付的 cache 重建費完全一樣。

用 Subagent 處理研究型任務

前面提到「研究型任務丟給 subagent」是省 token 的關鍵之一。為什麼有效?因為 subagent 跑在獨立的上下文裡:它讀進去的所有原始檔案、grep 結果、API 回應通通只存在於 subagent 自己的對話脈絡,主對話只會收到 subagent 的最終結論。對主對話的快取來說,這等於「用一筆有界的 subagent 成本,換到主上下文持續精簡」——主對話的前綴不會因為塞進大量檔案內容而暴漲,後續輪次都還能持續命中快取。

把這個過程畫出來,subagent 期間的總用量會像閃電一樣瞬間衝高、又瞬間落下:subagent 在獨立 context 裡讀越多檔案,總 token 用量就越高;但 subagent 一結束,那段獨立 context 整個釋放掉,只有最終摘要會回到主對話。如果是直接在主對話裡讀同樣的檔案,這些內容會永久累積在主上下文裡——後續每一輪都要付這份歷史的 cache read 費。

Subagent 與直接讀檔的上下文用量比較示意圖:綠色實線代表用 subagent 探索檔案,在執行區間瞬間衝到 80k 巔峰後立刻掉回 12k 並維持低水位;紅色虛線代表主對話直接讀檔,總用量持續線性成長到 92k
用 subagent 時總用量會出現「閃電」形狀的瞬間高峰再瞬間落下,主對話的長期用量被壓到很低;直接讀檔則是線性累積,後續每一輪都要為這些歷史付費。

什麼任務適合丟 Subagent

判斷準則一句話:「要讀很多、但結論很短」就丟 subagent。常見適合的場景:

  • 跨檔搜尋程式碼裡某個 symbol/某段邏輯的所有引用位置
  • 讀完一份長的 RFC、設計文件、第三方 API 文件後給簡短摘要
  • 掃過整個 repo 找出哪些檔案還在用舊版 API、舊套件
  • 跑多次 findgrep 嘗試不同關鍵字才能定位的探索性搜尋
  • 需要瀏覽多個目錄結構才能回答「這個專案大致怎麼組織」的問題

反過來,明確知道要改哪裡的編輯任務不要丟 subagent。Subagent 拿不到主對話的上下文,把實作任務丟過去等於要重新交代背景,反而更花 token;而且實作完成後 subagent 回傳的內容主對話還要重新理解一次,沒比自己直接動手便宜。

內建 Subagent 怎麼叫出來

Claude Code 內建幾個 subagent,最常用的三個:

Subagent用途什麼時候叫它
Explore快速程式碼搜尋要找符號定義、追引用、locate files 時
general-purpose通用研究、多步驟任務需要多步驟搜尋、整合不同來源結論時
Plan規劃實作步驟動手前先請它把流程列出來討論

不需要記指令,直接用自然語言講就會觸發:

  • 「用 Explore agent 幫我找專案裡所有 useAuth 的呼叫位置」
  • 「開個 subagent 讀完 docs/architecture.mdRFC-042.md,給我 200 字摘要」
  • 「跨整個 codebase 找出哪些檔案還在用舊版 logger,列出檔案路徑就好」

Claude Code 看到這類描述會主動用 Task 工具呼叫對應的 agent,主對話最後只會看到結論。如果不確定該用哪個 agent,講「開個 subagent 處理 ⋯⋯」就好,Claude Code 會自己挑合適的。

寫自己的 Custom Subagent

同一個流程會反覆需要時,可以寫成自訂 subagent,下次叫得更精準。把 markdown 檔放到 ~/.claude/agents/<name>.md(全域)或 .claude/agents/<name>.md(當前專案):

---
name: api-doc-reader
description: 讀完 docs/api 下的 OpenAPI 文件,回傳指定端點的參數與範例
tools: Read, Grep, Glob
---

當被叫來時:

1. 用 Glob 找出 docs/api/ 底下所有 .yaml、.md 檔案
2. Read 全部檔案內容
3. 找出指定端點(從使用者輸入推斷)
4. 回傳「參數列表 + 一個 curl 範例」即可,不要轉貼整段原文

name 是識別名稱,description 用來告訴 Claude「什麼情境要叫這個 agent」,tools 限制這個 agent 能用哪些工具(不寫就繼承所有工具)。寫好之後在主對話講「用 api-doc-reader 找 /users/:id 的細節」就會被觸發,跟內建 agent 用法一致。

注意:Subagent 看不到主對話的歷史,叫它的時候要把必要前提一次講清楚。例如「用 Explore 找 handleClick 的定義」要補上「我們在 React 專案、檔案大多在 src/ 下」這類資訊,不然 subagent 可能會在無關目錄裡白繞一圈。

用 statusline.sh 觀察快取狀態

知道這些原理之後,下一個問題是怎麼即時觀察。Claude Code 提供 statusLine 設定,每次模型回應結束會把 session 資訊(包含 token 用量、cache 狀況)以 JSON 從 stdin 餵給指定腳本,stdout 印出來的內容就是狀態列。我自己寫了一份 ~/.claude/statusline.sh,把幾個關鍵欄位拉出來顯示。

設定方法

~/.claude/settings.json 加上:

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh",
    "padding": 0,
    "refreshInterval": 10
  }
}

refreshInterval 是這份設定的關鍵:預設情況下 statusline 只會在每次 Claude 回應結束時更新一次,所以「離開電腦十分鐘再回來」狀態列上的快取剩餘時間還停在十分鐘前的數字,看不出快取是不是已經過期。設成 10 代表每 10 秒重新跑一次腳本,這樣即使沒有發出新訊息,狀態列也會持續更新「距離上次 API 回應幾秒」這個數字,超過快取 TTL 前變紅色提醒。

腳本核心邏輯

腳本第一行是計畫類型,後面三組門檻決定何時把對應數字變紅:

#!/bin/sh
# plan_mode: set to "pro" or "max"
plan_mode="max"

# 快取過期紅字門檻(秒);0 = 永不變紅
# pro 方案 prompt cache 約 5 分鐘過期,max 方案約 1 小時,超過就要重算 cache
case "$plan_mode" in
  pro) ago_red_threshold=270 ;;   # 4m30s,留 30 秒緩衝
  max) ago_red_threshold=3300 ;;  # 55m,留 5 分鐘緩衝
  *)   ago_red_threshold=0 ;;
esac

# Cache hit rate 紅字門檻(%);低於此值顯示紅字提醒
hit_red_threshold=70

# Context window 紅字門檻(%);高於此值顯示紅字提醒
ctx_red_threshold=50

三個門檻各自代表不同警示。ago_red_threshold 是「距離上次 API 回應幾秒」變紅的門檻,pro 設 270 秒(5 分鐘 TTL 留 30 秒緩衝),max 設 3300 秒(1 小時 TTL 留 5 分鐘緩衝)。hit_red_threshold 設 70 代表 cache hit rate 低於 70% 就把該欄整個變紅,提示這輪 cache 出了問題。ctx_red_threshold 設 50 代表上下文(context window)用量超過 50% 就提醒,留下足夠空間在 auto-compact 觸發前自己決定要 /clear 還是收尾。

腳本會從 stdin 解析 JSON、把模型名稱、session/weekly 額度、累積成本、上下文百分比印在第一行,第二行則印 cache hit rate、cache write/read、新輸入/輸出 token、上次 API 更新時間,以及距離現在多久。最後那個「多久前」的時間是靠把上一次 token 數字記在 /tmp/claude-statusline/ 比對得出,token 數字一改就代表 API 有新回應,時間戳跟著更新。

完整腳本

#!/bin/sh
plan_mode="max"

case "$plan_mode" in
  pro) ago_red_threshold=270 ;;
  max) ago_red_threshold=3300 ;;
  *)   ago_red_threshold=0 ;;
esac
hit_red_threshold=70
ctx_red_threshold=50

input=$(cat)

model_full=$(echo "$input" | jq -r '.model.display_name // empty')
model_short=$(echo "$model_full" | sed 's/ *([^)]*)//g; s/^[Cc]laude *//;' | tr '[:upper:]' '[:lower:]')

session_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
weekly_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty')

cwd=$(echo "$input" | jq -r '.workspace.current_dir // empty')
git_branch=""
if [ -n "$cwd" ]; then
  git_branch=$(GIT_OPTIONAL_LOCKS=0 git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null)
fi

parts="${model_short}"
if [ -n "$session_pct" ] && [ -n "$weekly_pct" ]; then
  parts="${parts} | session: $(printf '%.0f' "$session_pct")% | weekly: $(printf '%.0f' "$weekly_pct")%"
fi
if [ -n "$cost_usd" ]; then
  parts="${parts} | $(printf '$%.2f' "$cost_usd")"
fi
if [ -n "$ctx_pct" ]; then
  ctx_int=$(printf '%.0f' "$ctx_pct")
  if [ "$ctx_int" -gt "$ctx_red_threshold" ]; then
    ctx_str=$(printf "\033[31mctx: %s%%\033[0m" "$ctx_int")
  else
    ctx_str="ctx: ${ctx_int}%"
  fi
  parts="${parts} | ${ctx_str}"
fi
if [ -n "$cwd" ]; then
  dir_name=$(basename "$cwd")
  parts="${parts} | ${dir_name}${git_branch:+ ($git_branch)}"
fi
printf "%s" "$parts"

# 第二行:token 與 cache 資訊
cache_write=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0')
cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
input_tokens=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0')
output_tokens=$(echo "$input" | jq -r '.context_window.current_usage.output_tokens // 0')

total_input=$((cache_read + cache_write + input_tokens))
if [ "$total_input" -gt 0 ]; then
  hit_rate=$(awk "BEGIN { printf \"%.0f\", $cache_read * 100 / $total_input }")
  if [ "$hit_rate" -lt "$hit_red_threshold" ]; then
    hit_str=$(printf "\033[31mhit: %s%%\033[0m | " "$hit_rate")
  else
    hit_str="hit: ${hit_rate}% | "
  fi

  # 用 token 數字當簽名追蹤上次 API 更新時間
  state_dir="/tmp/claude-statusline"
  mkdir -p "$state_dir" 2>/dev/null
  session_id=$(echo "$input" | jq -r '.session_id // empty')
  state_file="${state_dir}/${session_id:-default}"
  current_sig="${cache_write}-${cache_read}-${input_tokens}-${output_tokens}"
  now=$(date +%s)
  last_update=$now
  if [ -f "$state_file" ]; then
    prev_sig=$(awk 'NR==1' "$state_file")
    prev_time=$(awk 'NR==2' "$state_file")
    [ "$current_sig" = "$prev_sig" ] && [ -n "$prev_time" ] && last_update=$prev_time
  fi
  [ "$last_update" = "$now" ] && printf "%s\n%s\n" "$current_sig" "$now" > "$state_file"

  elapsed=$((now - last_update))
  if [ "$elapsed" -lt 60 ]; then
    ago_str="${elapsed}s"
  elif [ "$elapsed" -lt 3600 ]; then
    ago_str="$((elapsed / 60))m$((elapsed % 60))s"
  else
    ago_str="$((elapsed / 3600))h$(((elapsed % 3600) / 60))m"
  fi

  if [ "$ago_red_threshold" -gt 0 ] && [ "$elapsed" -ge "$ago_red_threshold" ]; then
    ago_display=$(printf "\033[31m(%s ago)\033[0m" "$ago_str")
  else
    ago_display="(${ago_str} ago)"
  fi

  printf "\n%scache: %s, %s | in: %s, out: %s | %s" \
    "$hit_str" "$cache_write" "$cache_read" "$input_tokens" "$output_tokens" "$ago_display"
fi

儲存到 ~/.claude/statusline.sh 之後記得 chmod +x,然後在 settings.json 設好 statusLine 跟 refreshInterval 就會出現在每次回應底下。

也可以讓 AI 幫忙產生跨平台版本

上面那份腳本是 POSIX sh 寫的,macOS 跟 Linux 直接能跑,但 date -rawk/tmp 路徑這些細節在 Windows 原生環境會踩雷。如果想在不同平台都用得上,可以把下面這份提示詞貼到 Claude Code(或任何能執行指令的 AI 助手),它會先偵測作業系統再決定要產 sh 還是 PowerShell:

請幫我建立一份 Claude Code 的 statusLine 腳本,要顯示以下資訊並支援我目前的作業系統。

## 偵測環境
請先用指令確認我目前的環境(macOS / Linux / Windows、shell 種類),再決定用哪種腳本格式:
- macOS / Linux / WSL / Git Bash → 寫成 POSIX sh 腳本(#!/bin/sh)
- Windows 原生(無 WSL/Git Bash) → 寫成 PowerShell 腳本(.ps1)
- 任何環境都需要 jq 來解析 JSON,請先檢查 jq 是否安裝;沒裝的話先給我安裝指令再繼續

## 輸入格式
Claude Code 會把以下 JSON 從 stdin 餵給腳本,腳本要解析需要的欄位:
- .model.display_name(例如 "Claude Opus 4.7 (1M context)")
- .output_style.name
- .rate_limits.five_hour.used_percentage
- .rate_limits.seven_day.used_percentage
- .context_window.used_percentage
- .context_window.current_usage.cache_creation_input_tokens
- .context_window.current_usage.cache_read_input_tokens
- .context_window.current_usage.input_tokens
- .context_window.current_usage.output_tokens
- .cost.total_cost_usd
- .workspace.current_dir
- .session_id

## 顯示內容(兩行)
**第一行**:{model} | session: X% | weekly: X% | $X.XX | ctx: X% | {dir} ({git_branch})
- model 去掉 "Claude " 前綴與括號內容並轉小寫(例:opus 4.7)
- ctx 超過門檻整段(含 "ctx:")變紅字

**第二行**:hit: X% | cache: {write}, {read} | in: X, out: X | (Xs ago)
- hit rate = cache_read / (cache_read + cache_write + input),低於門檻整段變紅
- (Xs ago) 是「距離上次 API 回應幾秒」,超過門檻變紅
- 「上次 API 回應」用以下方法判斷:把當輪 token 數字拼成簽名存進暫存檔,下次執行比對簽名相同就沿用之前的時間戳;不同就更新成現在時間
  - macOS / Linux:暫存檔放 /tmp/claude-statusline/{session_id}
  - Windows:放 %TEMP%\claude-statusline\{session_id}

## 開頭可調門檻
腳本最上方留三組變數方便調整,附中文註解:
- plan_mode = "max"        # 切換 pro / max
- ago_red_threshold        # plan_mode=pro → 270(5min TTL 留 30 秒緩衝)
                           # plan_mode=max → 3300(1hr TTL 留 5 分鐘緩衝)
                           # 0 = 永不變紅
- hit_red_threshold = 70   # cache hit rate 低於此值變紅
- ctx_red_threshold = 50   # context 用量超過此值變紅

## 紅字輸出
- 終端機 ANSI:\033[31m...\033[0m(POSIX sh)
- PowerShell:用 $([char]27)[31m...$([char]27)[0m(注意 statusLine 收的是 stdout,PowerShell 要把 ANSI 字串組進輸出)

## 跨平台細節
- date 取秒數時間戳:macOS/Linux 用 date +%s;PowerShell 用 [int][double]::Parse((Get-Date -UFormat %s))
- 把秒數轉回 HH:MM:SS:macOS 用 date -r、Linux 用 date -d "@...",腳本要兩者都試
- awk 在 Windows 原生沒有,PowerShell 版改用內建運算
- 確保純 ASCII,避免 BOM 或 CRLF 在 sh 上炸掉

## 安裝後驗證
產生完腳本要:
1. 給 chmod +x(POSIX)或設定 ExecutionPolicy(PowerShell)
2. 在 ~/.claude/settings.json(macOS/Linux)或 %USERPROFILE%\.claude\settings.json(Windows)加上:
   {
     "statusLine": {
       "type": "command",
       "command": "<腳本絕對路徑>",
       "padding": 0,
       "refreshInterval": 10
     }
   }
3. 用 echo '{...}' | <腳本> 餵一段假 JSON 測一次,確認輸出正常

這份提示詞的重點是先偵測環境再決定腳本格式、明確列出 JSON 欄位避免 AI 自己編 schema,並且把跨平台地雷(dateawk、暫存路徑)一次提醒清楚。產出的腳本不會只在 macOS 能跑,PowerShell 版的 ANSI 顏色處理也會考慮到。

解讀狀態列

實際運作起來大致像這樣(以 Max 方案為例):

opus 4.7 | session: 32% | weekly: 18% | $4.21 | ctx: 23% | project (main)
hit: 96% | cache: 1240, 38500 | in: 320, out: 580 | (12s ago)

每個欄位代表的意思整理在下表:

欄位意義怎麼判讀
opus 4.7目前使用的模型切換模型會打破快取,注意不要為了比較頻繁切換
session: 32%5 小時 session 額度已用比例接近 100% 就會被強制等待重置
weekly: 18%7 天週額度已用比例長期工作節奏的參考;週末跑大任務前先看一下
$4.21本 session 累積成本(USD)方案內不額外付費,但可以拿來估算 API 直接呼叫的等價成本
ctx: 23%當前上下文(context window)用量超過設定門檻變紅,提醒接近 auto-compact,建議 /clear 或收尾
project (main)當前工作目錄與 git 分支確認自己在對的專案與分支上工作
hit: 96%本輪 cache hit rate90% 以上健康;低於 70% 變紅,代表這輪有大量內容被當新輸入重算
cache: 1240, 38500cache write, cache read tokenswrite 飆高代表前綴被打破;read 大表示有效利用快取
in: 320本輪 fresh input tokens沒被快取的新內容,例如使用者剛輸入的訊息與工具回傳
out: 580本輪 output tokens模型回應產生的 token,單價最貴
(12s ago)距離上次 API 回應的時間超過快取 TTL 門檻變紅,下一輪會付一次完整重建費

幾個常見的紅字組合可以這樣判讀:hit 整段變紅、cache write 飆高,幾乎可以確定剛才動了 CLAUDE.md、改了 skill、切了模型或 compact 過。ctx 變紅代表接近 auto-compact 門檻,建議切換任務或 /clear(12s ago) 變成 (56m ago) 且整段變紅,代表快取已經到期、下次發訊息會付一次完整重建費。

這邊實際示範,並且調整了變紅字的用量與時間,方便看到警示效果。想要更顯眼可以讓 AI 幫忙改成紅底白字之類,更顯眼的顯示方式。

小結

Claude Code 的計費邏輯把「對話歷史」轉換成 cache read/cache write/fresh input 三種不同單價的 token,是否吃到 cache 直接決定一段對話的成本。Prompt cache 走前綴比對 + TTL 過期,所以幾乎所有「省 token 的撇步」核心都是同一件事:避免讓前綴變動、避免讓 TTL 流逝。CLAUDE.md 寫精簡、用 Skills 取代靜態指令、任務切換時主動 /clear、用 subagent 隔離雜訊,每一條都是這個原則的應用。

點兩下 ESC 倒帶不會立刻摧毀快取,但被丟掉的那段 cache write 就是白付的,使用前可以先評估是不是用「再追加一句修正」就能達到同樣效果。最後,把 statusline.sh 配合 refreshInterval: 10 接起來,hit rate、上下文用量、快取剩餘時間就會持續顯示在眼前,比起發現額度燒光才回頭找原因,主動觀察狀態列才是長期最省成本的做法。


Sponsored Links