它不是壞掉,是被計時器砍掉:Claude Code 的 timeout 與輸出上限
寫於 2026 年 8 月 16 日(補 8 月 14 日的排程),9 月才上線(部落格的發佈額度 8 月 10 日就用完了,這批稿子要等到九月才發得出去)。文中的版本號與「目前」都指 8 月 16 日查到的狀態,Claude Code 更新很快,你讀到時可能已經又改過了。
Bash 指令跑到一半就沒了的那一秒,Claude Code 內部到底是誰按下停止鍵的?
這問題聽起來像在抓 bug,其實要找的是一個鬧鐘。而且不只一個。
廚房出餐是一模一樣的結構。一張單子進去之後,同時有好幾個鬧鐘在跑:爐子上的定時器管這道菜燉多久,外場的等餐鈴管客人坐在位子上多久沒東西吃,外送平台那邊還有一個接單倒數。哪個先響,這張單就以那個理由被撤掉。客人只知道「菜沒來」,完全看不出來是哪個鈴響的。
工具呼叫就是那張單。要看懂它為什麼消失,得先看懂鬧鐘是怎麼綁上去的。
綁法只有四種
把 Claude Code 所有的計時器攤開,會發現它們的差別不在「幾秒」,在「從什麼時候開始數」。就四種:
綁在開始執行上的,叫 wall-clock。從指令跑起來的那一刻起算,中間有沒有動靜都不管,時間到就到。
綁在建立連線上的,管的是「這台 server 打不打得通」,跟工具跑多久完全無關。
綁在送出請求到收到第一個 byte 上的,只看回應的頭,不看回應的尾。一個回應慢慢吐三小時,這道計時器是滿意的;一個回應在 61 秒後才吐第一個字,這道計時器已經翻臉了。
綁在上一次有動靜到現在上的,叫 idle。每次有回應或進度通知就歸零重數。
wall-clock 跟 idle 的差別,是停車費跟自動門的差別。停車費從你停進去開始算,你有沒有回來看車不影響它跳表;自動門的感應器則是有人經過就重新倒數,沒人經過才會關。一個工具呼叫同時被這兩種盯著,誰先到期誰先動手。
搞懂這四種綁法,下面那張表就不用背了:它就是四個綁法乘上兩個執行層(Bash 與 MCP)的排列組合。
每一道的實際數字
| 這道計時器 | 從什麼時候開始數 | 預設值 | 用什麼調 |
|---|---|---|---|
| Bash 指令執行 | 指令開始跑 | 120000 ms(2 分鐘) | BASH_DEFAULT_TIMEOUT_MS |
| Bash 天花板 | 模型自己能要多長 | 600000 ms(10 分鐘) | BASH_MAX_TIMEOUT_MS |
| MCP server 啟動 | 開始連線 | 30000 ms(30 秒) | MCP_TIMEOUT |
| MCP 啟動批次等待 | blocking 啟動時等整批連線 | 5000 ms | MCP_CONNECT_TIMEOUT_MS |
| MCP 工具執行 | 工具開始跑 | 100000000 ms(約 28 小時) | MCP_TOOL_TIMEOUT,或各 server 的 timeout |
| MCP 單次請求 | 送出到第一個回應 byte | 60 秒(只有 HTTP / SSE / claude.ai connector 有) | 沒有自己的變數,只能被上面兩個往上拉 |
| MCP 閒置 | 上一次有動靜 | 網路型 300000 ms(5 分鐘)、stdio 1800000 ms(30 分鐘) | CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT |
| 模型 API 請求 | 送出請求 | 600000 ms(10 分鐘,上限 2147483647) | API_TIMEOUT_MS |
這八道是會把一次工具呼叫砍斷的。官方 env-vars 頁上名字裡帶 TIMEOUT 的變數,我數了一下有 24 個,其中一個(CLAUDE_CODE_CONNECT_TIMEOUT_MS)已經標明在 v2.1.186 移除、現在是 no-op。剩下的多半在管 OTel 送出、plugin 安裝、Glob 檔案搜尋這類不會讓你懷疑人生的東西,所以沒進表。
互相踩到的地方
數字背下來沒用,會咬人的是它們之間的關係。
天花板取兩者大的那個。 BASH_MAX_TIMEOUT_MS 讀起來像絕對上限,官方寫的是「The effective ceiling is the larger of this and BASH_DEFAULT_TIMEOUT_MS」。你為了保護 CI 機器把 MAX 調成 30 秒、DEFAULT 忘了動,真正的天花板還是 2 分鐘。要收緊得兩個一起收。
逾時的 Bash 指令不會死,會被丟到背景。 這是最容易誤判的一項。指令跑到時間沒跑完,Claude Code 把它移進背景任務繼續跑,工具結果裡直接告訴你:Command did not complete within its 120s timeout and was moved to the background,後面接 task ID 跟輸出檔的路徑。只有三種指令會真的被停掉:以 sleep 開頭的、指令裡任何地方出現 git 的、以及 Claude Code 沒辦法完整拆解成單一指令的複合指令。
同一個數字寫在兩個地方,行為不一樣。 .mcp.json 裡某台 server 的 timeout 欄位寫 500,官方說法是「values below 1000 are ignored」,直接落回 MCP_TOOL_TIMEOUT,也就是那個 28 小時的預設;但環境變數 MCP_TOOL_TIMEOUT 自己寫 500,是被進位成 1 秒。你以為在收緊,實際上是在放行到隔天中午。(v2.1.162 之前,per-server 那邊也是進位成 1 秒,不是忽略。)
60 秒那道只往上拉,不往下縮。 HTTP、SSE、claude.ai connector 才有的 per-request 計時器,預設 60 秒。把 per-server timeout 或 MCP_TOOL_TIMEOUT 設到 60000 以上,它會跟著升到那個值;設更低不會讓它變快。而且 MCP_TOOL_TIMEOUT 沒設時的那個 28 小時預設,從來不會餵給這道計時器。stdio 與 WebSocket server 完全沒有這道。
兩分鐘之後換場地,但計時器沒停。 主對話裡的 MCP 工具呼叫跑超過兩分鐘會自動轉成背景任務(CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS,預設 120000,需要 v2.1.212 以上),Claude 拿到 task ID 就繼續做別的事,你在 /tasks 看得到也停得掉,離開 session 就不留存。轉背景不等於刑期暫緩:wall-clock 跟 idle 兩個上限在背景照算。
per-server 的 timeout 順便當了 idle 的地板。 只要它 ≥ 1000,Claude Code 就不會用「閒置」的理由比這個值更早中止那台 server 的呼叫(v2.1.203 以上)。所以某台 server 老是被 idle 砍,調它的 timeout 是有用的,不用去動全域的 idle 變數。
輸出上限是另一套規則
計時器管什麼時候停,輸出上限管停下來之後你看得到多少。這兩套是分開的,而且各自有陷阱。
Bash 這邊,指令的輸出邊跑邊寫進一個工作檔,超過 5 GB 直接砍。跑完之後 Claude Code 從那個檔案讀回來,BASH_MAX_OUTPUT_LENGTH 決定讀回窗有多大,預設 30000 字元、硬上限 150000。
真正反直覺的是:成功跟失敗的內嵌上限不一樣。判定為 valid 的結果,大約 30,000 字元以內直接內嵌,超過就換成一個存在 session 目錄的檔案路徑加一小段開頭預覽;判定為 failure 的結果,大約 10,000 字元以內內嵌,超過只給頭尾摘錄,連檔案路徑都沒有。你的測試爆掉、噴出六萬行 log,那六萬行不會留給你一個檔案去翻,只剩頭尾。
哪些算 valid?exit 1 只有在 grep、rg、egrep、fgrep、find、diff、test、[,加上 git diff 跟 git grep 這幾個上面才會被當成正常結果。其他指令 exit 1 一律算 failure,即使那個 1 在語意上完全無害:pgrep 沒找到、jq -e 沒命中、cmp 發現檔案有差異,全部掉進 10,000 字元那道窄門。
還有一個容易調錯的地方:把 BASH_MAX_OUTPUT_LENGTH 拉到 150000,放大的是讀回窗,不是內嵌上限。一個超過 30,000 字元的成功結果,不管你怎麼調都還是回你一個檔案路徑加預覽。
MCP 這邊只有兩個數字。輸出超過 10,000 tokens 會跳警告,這個門檻是寫死的;上限預設 25,000 tokens,用 MAX_MCP_OUTPUT_TOKENS 調。例外是有宣告 anthropic/maxResultSizeChars 的工具,它的文字內容改吃自己宣告的字元上限(硬天花板 500,000 字元),但只要那個工具回傳的是圖片資料,一樣吃 MAX_MCP_OUTPUT_TOKENS。所以某個回圖的 MCP 工具老是被截,調 MAX_MCP_OUTPUT_TOKENS 才有用,去拜託作者加標註沒有用。
設在哪裡,以及誰壓過誰
兩個地方可以設。shell 裡 export 之後再開 claude:
1 | export BASH_DEFAULT_TIMEOUT_MS="300000" |
或者寫進 settings 檔的 env 區塊。全域放 ~/.claude/settings.json,只給某個專案就放 .claude/settings.json,只給自己這台機器放 .claude/settings.local.json:
1 | { |
某一台 MCP server 要單獨放寬,寫在它自己的 .mcp.json 條目裡,單位是毫秒:
1 | { |
覆蓋順序有三層,記反了會很痛苦。settings 檔之間是 Managed > 命令列參數 > Local > Project > User。環境變數壓過同名的 settings key(例如 ANTHROPIC_MODEL 壓過 model)。而 shell 跟 settings 檔的 env 撞在一起時,贏的是 settings 檔,跟一般直覺相反:Claude Code 啟動時會把每個 env 條目寫進 process environment,蓋掉從 shell 繼承來的值。你在 .zshrc 裡 export 了半天沒反應,先去看 settings 檔的 env 有沒有同名的那行。
改完怎麼確認生效?Bash 那道最好驗,因為逾時訊息會把實際套用的秒數寫在裡面:看到 within its 120s timeout 就代表你的 300000 沒吃到,看到 300s 就成了。MCP 的連線狀態看 /mcp。順帶一提,shell 的變數只在啟動時讀一次,改了要重開 claude;settings 檔的 env 則是檔案一變就重新套用到執行中的 session。
誠實邊界
上面每一個變數名、預設值、單位、版本號,都是 8 月 16 日回 Claude Code 官方文件的 env-vars、mcp、settings、tools-reference 四頁逐項核對出來的,數字照抄原文。我沒有逐一實測每一層——沒有真的去架一台會卡在第 59 秒吐第一個 byte 的 HTTP server 來驗那道 60 秒的計時器。
有一項我原本要寫、查完之後整個拿掉:llmTimeout。二手整理說 settings 清單裡有這個 key,我回官方 settings 頁抓下來 grep,一次都沒命中。兩邊對不上就不寫,寧可少一項。
還有一組我刻意沒放進表:askUserQuestionTimeout(預設 never,也就是沒人回就一直等)跟 dialogExpiry(預設 5m)。它們管的是等人,不是等程式,而且官方明講權限提示與 AskUserQuestion 走自己的流程、不受 dialogExpiry 管。混進計時器那張表只會讓人誤會。
這個結構到處都是
回到廚房那個比喻。你不會問「為什麼菜沒來」,你會問「這疊鬧鐘裡,哪一個最短」。
這套「一疊互不知情的計時器」根本不是 Claude Code 的特產。nginx 的 proxy_connect_timeout、proxy_send_timeout、proxy_read_timeout 是同一組綁法;JDBC 連線池的 connectionTimeout 對上 socketTimeout 對上 queryTimeout 也是;Kubernetes 的 liveness 與 readiness probe,本質就是把 idle 那種綁法做成了外部探針。全部都是連線、單次請求、整段執行、閒置這四種綁法的排列組合,差別只在誰家的預設值比較兇。
所以下次碰到「某個東西跑一半沒了」,第一個動作不是去讀那段程式碼。先把這一層堆疊裡所有的計時器列出來,看哪一道最短,再看你調的那個是不是最短的那一道。八成不是。

























































































































































































