Windows 11,Sublime Text 裡跑著一個叫 GhostShell 的終端機,底層是 libghostty。使用者 dpc00 在 anthropics/claude-code 開了 issue #101114,標題就是他卡住的地方:終端機明明回答了探測,Claude Code 2.1.296 還是沒送狀態回報。他看得到 Claude Code 啟動時丟出來的幾個問題,也確認終端機照順序回了。可是跑完一整輪,一次工具呼叫加一個最終回答,終端機一筆狀態都沒收到。換了三個 session,結果一樣。

他在 issue 裡問:

What else must be true for Claude Code to start sending status reports? Is it behind a rollout flag or a particular mode?

照他後來自己查到的結果,答案跟 flag 無關,問題出在他的終端機和 Claude Code 之間那一層。要看懂卡在哪,得先回答一個更底層的問題:Claude Code 是怎麼判斷「這個終端機聽得懂我」的?

在這之前,終端機只能用猜的

Claude Code 2.1.295 的 changelog 寫的是 “Added Program Status Protocol (OSC 7501) support: terminals that implement it can show whether Claude Code is working, waiting on you, or done”。這版上 npm 的時間是 UTC 2026 年 10 月 8 日。

沒有這個協定的時候,想知道 Claude Code 在不在忙的工具只能猜。協定作者 Mitchell Hashimoto 在 10 月 6 日的說明文裡舉了一個例子:Herdr 判斷 Claude Code 狀態的規則有 16 條,第一條是看視窗標題開頭的字元。2.1.227 以前是點字 spinner,2.1.228 起換成半圓字元。他說那份規則檔光是為了 Claude Code,三個月內就改了十次。

這很像診所候診。護理師看不到診間,只能隔著門聽醫生有沒有在講話,來猜下一個什麼時候輪到。醫生哪天換了習慣,安靜地寫病歷,護理師就判斷錯了。比較省事的做法,是讓醫生自己按門口的燈。作者的說法只有一句:”The program already knows.”

它講的那句話長什麼樣

規格目前還在改,下面全部以 0.4 版(2026 年 10 月 10 日)為準。整個協定只有兩種序列,一種是回報,一種是探測:

1
2
3
4
OSC 7501 ; pairs ST        report
OSC 7501 ; ? ST feature detection

ESC ] 7501 ; state=blocked:kind=permission:app=terraform:msg=QXBwbHkgMyB0byBhZGQsIDEgdG8gY2hhbmdlLCAwIHRvIGRlc3Ryb3k/ ESC \

最後一行是規格自己的範例。pairs 是一串 key=value,彼此用冒號隔開,ST 可以是 ESC \ 或 BEL。value 的字元集刻意排除冒號和分號,所以完全不用跳脫。要放人看的文字時,msg 和 title 一律用 base64 包起來,範例那串解開就是 Terraform 在問 “Apply 3 to add, 1 to change, 0 to destroy?”。

state 是唯一必填的 key,合法值只有五個:idle、working、done、blocked、error,另外有一個 clear 用來刪記錄,它本身不算狀態。blocked 可以再帶 kind,說明在等你什麼,也只有三種:permission、question、auth。

有一條規則我覺得很聰明:沒帶 state,或 state 是終端機不認得的值,整筆回報直接丟掉。規格寫的理由是,日後新增的狀態送到舊終端機上,不會被誤當成 idle。寧可什麼都不顯示,也不要顯示錯的。

至於 Claude Code 在什麼時機送哪一個 state,官方文件只說會顯示 “working, waiting on you, or done”,對應到哪個值、帶不帶 kind,我沒有查到原始資料,這裡不猜。

done 活得比 working 久

終端機替每個 id 保存一筆記錄,沒有 id 的就是根記錄。每次回報會整筆取代舊的,這次沒帶的 key 之後就不存在了,所以 app 這種識別名每次都要重帶。沒有心跳,終端機也不准要求程式重送來保活。

那記錄什麼時候消失?規格原文:

When the process attached to the terminal exits, or a new shell prompt begins (OSC 133 A), the terminal MUST drop working and blocked records and MAY drop idle ones. done and error survive both events.

程式結束、shell 跳出新提示字元,「正在跑」和「在等你」一定會被清掉,這很合理,跑的人都不在了。可是 done 和 error 兩種情況都不會被清。規格對 done 的定義是「結果好了,使用者還沒看」,它活得久,靠的就是後半句。

想想洗衣機。正在洗的燈,拔掉插頭就熄了;「洗好了」那個提示,要等你真的去開門才算數。你在別的分頁忙了二十分鐘,回來時 Claude Code 早就跑完、shell 都回到提示字元了,這時候還留著的那個 done,才是你最需要看到的東西。

這也是它跟既有序列分得開的地方。規格自己比較過:OSC 9;4 只表達忙碌、百分比、錯誤,沒有「在等使用者」;OSC 9、99、777 這類桌面通知是一次性事件,通知發完,終端機就不知道現況了;OSC 133 是 shell 送的,只知道指令在跑或跑完,不知道程式在裡面做什麼(之前寫螢幕閱讀器模式時碰過它);視窗標題則是一個沒結構的字串,就是前面那 16 條規則在猜的東西。

安全上還有一條限制:記錄只能寫、不能讀,”there is no way to read records”。終端機唯一會寫回程式的,就是探測的回覆。

先問一句,再補一句人人都會答的

探測本身很短。程式送 OSC 7501 ; ? ST,支援的終端機回一個 ?,0.4 版起還可以附上自己支援的 states 和 kinds 清單。規格也說探測是選用的,終端機本來就必須忽略看不懂的 OSC,程式不問直接送也不會出事。

麻煩在「沒回答」要怎麼判斷。等多久才算沒回?規格給的解法是這句:

To avoid waiting, send primary device attributes (CSI c) right after the query. Every terminal answers that, so if its reply arrives first, the protocol is unsupported.

CSI c 是 primary device attributes,簡稱 DA1,規格說每個終端機都會回。把它緊接在 7501 的問題後面送出去,就像在銀行櫃檯一口氣問兩件事:「你們能辦外幣換匯嗎?」「現在幾點?」櫃員會照順序回答。如果他直接告訴你現在幾點,那第一題他就是跳過了,你不用站在那裡等。

回頭看 dpc00 貼的啟動序列,Claude Code 就是這樣做的:

1
2
3
4
ESC[>0q
ESC[?u
ESC]7501;?
CSI c

這四個是 issue 原文列出的順序,他說終端機也照這個順序回答。前兩個問題跟 7501 無關,這裡不展開。重點是 7501 的問題後面緊跟著 CSI c,就是規格建議的那個哨兵。

回到 ConPTY:答案在半路被吃掉了

所以 dpc00 的終端機明明回了 ?,Claude Code 為什麼還是判定不支援?

他後來在同一個 issue 裡自己找到了原因,以下是他的說法,Anthropic 沒有出面確認。Windows 的 ConPTY 有兩個行為:一是 “silently drops any OSC sequence written to a program’s input, so the 7501 reply never arrived”,終端機寫給程式的 OSC 回覆,ConPTY 會默默丟掉;二是 ConPTY 會自己回答終端機能力查詢,所以 Claude Code 在 GhostShell 的回答抵達之前,就先看到了「問題問完了」。

拿哨兵規則來對,兩件事剛好疊在一起。7501 的回答根本沒送到,哨兵的回答又被中間那層搶先代答。從 Claude Code 這邊看,就是哨兵先回來、7501 沒回應,照規格該判定不支援。

這個 issue 裡最值得記的,是他走過的那條死路。他一開始問的是 rollout flag 或特定模式,往 Claude Code 自己的設定去找;真正的問題在傳輸層。終端機這端看到的是「我回答了」,程式那端收到的卻是另一回事。他的解法是改用 Windows Terminal 的 ConPTY(modern_conpty 這個設定),並把 7501 的回覆改成以 “keyboard-style input events” 送出。之後 claude --debug 顯示 programStatus=yes,終端機開始收到 idle、working、done。

同一串還有一則留言,來自一個自稱 AI agent 的帳號,不是維護者。它說 claude --debug 之後去看 ~/.claude/debug/latest 的 “Terminal capabilities” 那一行,沒收到回覆時會寫 programStatus=no (probe: no reply to OSC 7501 ; ?)。這不是官方文件寫的,我也沒驗證過,要用請自己對照。

tmux 裡卡在同一個哨兵

官方文件「See session status in your terminal」那一節,列了四個看不到狀態的原因。其中一個是 tmux:

“inside tmux, Claude Code checks tmux for support instead of your terminal, and allow-passthrough has no effect on that. Start the session outside tmux.”

在 tmux 裡,Claude Code 問的對象是 tmux,不是你外面那個終端機。tmux 怎麼回?tmux issue #5713 在 10 月 9 日被開出來,回報者的環境是 tmux 3.7c 加 foot 1.27.0。他貼的 log 是一個程式送出 OSC 7501 ; ? 再送 CSI c 之後,tmux 這邊的紀錄:

1
2
input_exit_osc: unknown '7501'
input_send_reply: \033[?1;2;4c

tmux 不認得 7501,直接略過;DA1 它倒是很盡責地自己回了。哨兵先到,程式判定不支援。跟 ConPTY 是同一種形狀,中間層替真正的終端機回答了哨兵。

allow-passthrough 為什麼救不了?文件說這個選項的作用,是讓 “notifications and progress updates reach the outer terminal instead of being swallowed by tmux”。它管的是往外送的東西,管不到「tmux 自己先回了 DA1」這件事。

tmux 維護者 nicm 在同一個 issue 的回應很直接:

This seems premature. Let’s see if any other terminals and programs pick it up.

他還補了一句,pane 標題、OSC 9 進度條、OSC 133 加起來,目前已經涵蓋了這類需求。我 10 月 12 日讀到時,這個 issue 還是 open。

另外三個原因比較單純。背景 session 不會把狀態回報給終端機,就算你正 attach 在上面也一樣,文件說狀態改在 Agent view 裡看。設了 CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 的話,Claude Code 不探測、也不回報;這個變數同時會關掉依對話內容自動更新標題,以及背景那個產生 session 標題的小模型請求。最後是版本,要 2.1.295 以上,用 claude --version 看。這裡有個小陷阱:10 月 12 日查 npm,latest 已經是 2.1.296,stable 這個 dist-tag 卻還停在 2.1.287。照 npm 標籤推算,跟著 stable 裝的人拿到的是 2.1.287,比 2.1.295 舊,所以目前還沒有這個功能。

哨兵只在沒人代答的時候成立

ConPTY 跟 tmux 拆完,回頭看那個 DA1 哨兵,它其實是這個協定裡最好用的一招。

「問一個新問題,緊跟一個人人都會答的舊問題,看哪個回答先到」,這招不只用在終端機。只要通道保證順序,就能用它判斷「對方支不支援」,不必挑一個逾時秒數乾等。可是它藏了一個前提:回答哨兵的,必須是那個你真正想問的對象。ConPTY 和 tmux 都打破了這個前提,它們站在中間,替後面的終端機答了哨兵,自己又不懂 7501。

所以哪天你在任何一層代理後面做能力偵測,結果跟預期不同,先問一句:剛才回答我的,是我以為的那個人嗎?

如果你住在 tmux 裡

主要在 tmux 裡開 Claude Code 的人,我認為現在不值得為了這盞燈換工作方式。官方給的唯一建議是「在 tmux 外面開 session」,而 tmux 維護者的態度是觀望。至於哪些終端機已經支援,作者自己提到 libghostty 和 Rex 兩個實作;tmux issue 裡有位留言者整理了 Ghostty 的 libghostty-vt parser、SwiftTerm 已經合併、cmux 有文件,這些我沒有逐一去各專案核對。官方文件的說法是去查你自己終端機的文件。

會讓我改變想法的條件有兩個:tmux #5713 真的被合併;或者你本來就習慣在支援的終端機裡開好幾個分頁,各跑一個 Claude Code,不經過 tmux。後者的話,正好是文件說它派得上用場的情境,”when you run long tasks or several sessions at once”。

想確認自己的終端機到底收不收得到,作者文中有一段完整的 shell 範例,用來包住 rsync:

1
2
3
4
5
6
status() {
printf '\e]7501;state=%s:msg=%s\e\\' "$1" "$(printf '%s' "$2" | base64 | tr -d '\n')"
}

status working "Syncing photos"
rsync -a ~/Photos backup:/photos && status done "Photos synced" || status error "rsync failed"

這段我沒有跑過。它沒先探測,直接送,按規格是合法的,終端機不支援就會默默忽略。所以畫面上沒反應,你也分不出是終端機不支援,還是中間有東西吃掉了。


這篇只讀了官方文件、changelog、規格與兩個 GitHub issue,沒有在任何終端機上跑過 Claude Code 2.1.295 以上的版本,也沒有抓過任何 OSC 7501 封包。規格從 0.1(9 月 28 日)到 0.4(10 月 10 日),十二天內出了四版,日後細節可能還會變動。

原文來源:Claude Code CHANGELOG(2.1.295 條目)、Terminal configuration - Claude Code Docs(See session status in your terminal、Configure tmux)、Environment variables - Claude Code Docs、Program Status Protocol 規格 0.4、Mitchell Hashimoto:Program Status OSC 7501、anthropics/claude-code#101114、tmux/tmux#5713。版本資訊以 2026 年 10 月 12 日 npm 查詢結果為準。