有些公司的規定是這樣的:工作站上每一個行程,都必須經過一支強制啟動器啟動。沙箱、網路管制、憑證注入,全部掛在那支 launcher 上。繞過它就是違反政策。

碰到 Claude Code,以前的做法很直覺。在 PATH 前面塞一支自己寫的 claude,讓它先進 launcher、再去跑真正的 binary。狠一點的做法是直接把 ~/.local/bin/claude 那個 symlink 換掉。使用者打 claude,路徑被接管,收工。

覆蓋率報表上這件事是 100%。

實際跑起來的東西不是這樣。agent view 裡每一列的 session、按需啟動的背景服務、服務當掉之後重新生出來的 session、Claude Code 為了裝完更新而對自己做的重啟,全都在 launcher 外面。

原因只有一句話:這些行程是 Claude Code 直接用 binary 的絕對路徑啟動的,不會去查 PATH

你在大樓門口設了檢查哨,門口那一關做得很嚴。但樓裡的人本來就知道自己辦公室在幾樓,他走的是內部電梯。檢查哨攔得住從外面進來的人,而 Claude Code 開出來的那些行程,從來沒有從外面進來過。

以前改的是入口,現在改的是「它怎麼開自己」

CLAUDE_CODE_PROCESS_WRAPPER 這個變數做的事,是把 launcher 交給 Claude Code 自己去套。從此它每衍生一個自己的行程,都先跑你那支腳本。

腳本本體可以很短:

1
2
3
4
#!/bin/sh
# Your organization's setup: enter the sandbox, apply
# network controls, or inject credentials.
exec "$@"

chmod +x 給執行權限,放一個絕對路徑,例如 /opt/corp/launcher

結尾那行 exec "$@" 不是裝飾。exec 的意思是「不要另外生一個行程,把我自己換掉」。腳本走進去,出來的是 Claude Code 本人,行程還是同一個行程。寫成 fork 一個子行程然後自己結束,就變成把小孩生出來、家長走掉:背景服務追蹤不到那個 Claude Code 行程,agent view 會把該 session 標成 failed 並在訊息裡點名你那支 launcher,然後服務去收屍。

如果你之前已經把 ~/.local/bin/claude 這個 symlink 換成自己的 launcher,改用這個變數的同一次變更裡要把原本的 symlink 還原。被換掉的 symlink 會讓第一個 wrapped session 同時透過兩支 launcher 啟動背景服務,而且會把安裝狀態變成 externally managed:/doctor 會回報、自動更新不會動那個檔、舊版清理會一直停用,直到安裝器重新接管那個路徑。

這個變數不能用 export 設

shell 裡 export 一次,只有這個 shell 開出來的東西吃得到。背景服務是按需啟動的、活得比你的 shell 久、而且從不重讀 shell profile。這個形狀在寫 proxy 與自訂 CA 那篇出現過一次,只是這次的主角更難纏:那次至少重開一個 session 就好,這次連重開 session 都不夠。

所以它要住在 settings 檔裡:

1
2
3
4
5
{
"env": {
"CLAUDE_CODE_PROCESS_WRAPPER": "/opt/corp/launcher"
}
}

單機放 ~/.claude/settings.json,全公司佈署放 managed settings。同樣的值也有一個具名的頂層設定鍵:

1
2
3
{
"processWrapper": "/opt/corp/launcher"
}

兩者都設的時候,CLAUDE_CODE_PROCESS_WRAPPER 優先。

形式 最低版本 低於此版本的行為
CLAUDE_CODE_PROCESS_WRAPPER 環境變數 v2.1.208 忽略變數,所有行程不包裝
processWrapper 設定鍵 v2.1.210 當成未知鍵忽略、不套用 launcher、且不報錯
Remote Control 啟動的 session 納入包裝 v2.1.210
agent teams 在 tmux/iTerm2 開的分割窗 teammate session 納入包裝 v2.1.210

第二列值得多看一眼。低於 v2.1.210 的 client 會把 processWrapper 當成未知鍵忽略,不套用 launcher,而且不報錯。一個安靜失效的合規設定比沒設更危險,因為報表上它是綠的。

設定推下去之後,舊的那批還在外面跑

已經在跑的服務與 session 只在啟動時讀一次那個變數。不重啟,它們會繼續開出沒包到的行程。

1
2
claude daemon stop --any     # 停掉「按需啟動」的服務;下一個需要它的指令(例 claude agents)會啟動 wrapped 的
claude daemon stop # 已安裝為系統服務(installed service)時用這個,不加 --any

停完服務,還要重啟你開著的 claude session。

沒辦法一台一台手動處理的機隊,文件給了一個緩衝:設定推送後啟動的第一個 session 會自動退役殘留的未包裝按需服務。前提是真的有人開了新 session。完全沒有新 session 的機器會一直留著舊服務,而 installed service 一定要照上面那步重啟。

以前靠 ps 認人,現在 ps 認不出來了

設了 launcher 之後,ps 跟活動監視器不再顯示 Claude Code 背景輔助行程的 claude bg-pty-hostclaude bg-spare 標籤。launcher 的 exec 重建了參數列,顯示名稱跟著被重寫。

文件在這裡先堵住誤會:行程本身沒變,而且 Claude Code 是用 binary 路徑辨識自己的行程,從不看顯示名稱。

值得玩味的是稽核這件事的反轉。行程監控的直覺是看名字對不對,而這個為了合規而設的東西,正好把名字洗掉了。

新的確認方式在別的地方:

1
2
/status                # session 內執行:「Self-exec」欄位顯示解析後的啟動指令,且在背景服務與設定不一致時會警告
claude daemon status # 從 shell 印同樣的資訊;連你把變數取消之後也印得出來(此時 /status 已不顯示該欄位)

claude daemon status 那個括號裡的性質很實用:要證明「我確實把它關掉了」,/status 是問不出來的,那一欄已經不在了。

真正要讀的是這一段:它包不到什麼

一個叫「強制啟動器」的機制,文件花最多篇幅寫的是它不強制的地方。要讀懂那份清單,得先把對照的那一份收攏起來——「包得到」的六類散在前面幾段跟版本表裡:按需啟動的背景服務、agent view 每一列的 session、服務當掉之後重新生出來的 session、Claude Code 為了裝完更新而對自己做的重啟、Remote Control 啟動的 session,還有 agent teams 在 tmux/iTerm2 開的分割窗 teammate session。

包不到的是下面五項。文件替其中三項給了補法,另外兩項只解釋了為什麼包不到。

包不到的破口 是誰啟動的 文件給的補法
設 launcher 之前就寫好 unit 的 installed 背景服務 launchdsystemd 從 unit file 啟動 讓服務帶著新設定重啟;/statusclaude daemon status 會在跑著的服務與設定的 launcher 不一致時警告
你自己在終端機打的那個 claude 你的鍵盤 PATH 更前面的目錄放一支叫 claude 的腳本,由它去跑 launcher 加真正的 binary;不要去換掉被管理的 symlink
claude-cli:// deep link 開出來的第一個行程 作業系統的協定處理器 disableDeepLinkRegistration 設定阻止註冊處理器
--worktree 搭配 --tmux 的那次重啟 終端多工器 文件未給
Claude in Chrome 註冊的 native-messaging host 瀏覽器 文件未給

第二列那條路把我們帶回文章開頭那個問題。有人會擔心這樣一來所有行程被包兩次,文件也先講了:自我衍生的行程不查 PATH,所以兩支 launcher 不會疊起來。deep link 那一列的破口只在最前面那個行程,該 session 之後在背景開的東西都會走 launcher;而第一列的服務一旦帶著新設定重啟,它生出來的 session 也會走 launcher。最後兩列沒有這種轉圜,文件講完「不是 Claude Code 的 binary 開的」就停了。

把這五項跟「包得到」的那份清單擺在一起,切線其實非常一致:凡是 Claude Code 從自己的 binary 啟動的,都包得到;凡是別人啟動的,都包不到。別人是誰?作業系統、終端多工器、瀏覽器、你的鍵盤。

agent teams 在 tmux 或 iTerm2 開的分割窗 teammate session 是驗證這條規律的好例子。那些窗格是互動式的、不是背景行程,照直覺該被歸到「使用者自己開的」那一類,但它包得到。文件特別停下來解釋了一句:因為那是 Claude Code 從自己的 binary 啟動的。決定包不包得到的從來不是「這個 session 長得像不像背景服務」,是「誰按下了啟動」。

Windows 上這個變數直接被忽略

launcher 契約依賴 exec,而 Windows 不支援。

後果不是「部分生效」。設了變數的 Windows 機器會全部不包裝地照常運作,唯一的訊號是 debug log 裡的一行警告。文件自己把話講死:如果你的 launcher 政策涵蓋 Windows,這個變數在那裡不滿足政策,規劃佈署的時候要把 Windows 機器算成未包裝。

順帶一提,launcher 跑不起來的時候,Claude Code 會拒絕啟動該行程,而不是不包裝地照跑。Windows 是這條規則的例外。

專案層設不了它,這是故意的

.claude/settings.json.claude/settings.local.json 不能設定 launcher。理由很直白:commit 進 repo 的檔案,不該有能力在這台機器每一個 Claude Code 行程前面插一支 binary。

實作上是兩種擋法。這兩個檔裡的 CLAUDE_CODE_PROCESS_WRAPPER 會被忽略並在 debug log 留警告;processWrapper 這個鍵則是根本不從那些檔讀。

另一頭,用 remote managed settings 推送 processWrapper 的組織,會在安全核准對話框上看到它跟其他「執行管理員提供的執行檔」的設定並列。具名設定的代價與好處都在這裡:它有名字,所以擋得住、也看得見。

那個名字很像的變數不是它

CLAUDE_CODE_PROCESS_WRAPPER CLAUDE_CODE_SHELL_PREFIX
包什麼 Claude Code 自己的行程 Claude 代你執行的 shell 指令(Bash 工具呼叫、hooks、啟動 stdio MCP server 的指令)
怎麼傳 指令以分開的 argv token 傳給 launcher 去 exec 每一條以單一個 shell-quoted 字串放在 $1,由 wrapper 重新求值

文件原話是「A launcher written for one doesn’t work as the other.」參數到手的形狀根本不一樣,一邊是陣列、一邊是字串,拿錯了不會有語意上的相容可言。

launcher 自己要守的六條

六條裡的第一條前面已經講完了:腳本要以 exec "$@" 結尾。剩下五條裡最容易寫壞的是環境變數那一條。繼承到的環境要原封不動傳給 exec:加東西可以(注入憑證就是加),掉東西不行。文件點名了三樣走繼承環境傳遞的東西:每個 session 的驗證 token、模型與供應商選擇、以及 CLAUDE_CODE_PROCESS_WRAPPER 自己。

用白名單重建環境的 launcher 會弄壞它自己啟動的 session,而且 /status 會回報 launcher mismatch。交接班的鑰匙串可以多掛一把上去,但只要換成一串自己配的新鑰匙,連自己的門都開不了。launcher 如果非得進入一個會重置環境的 namespace 或沙箱,要在裡面逐字重新 export 繼承到的環境。

第二個容易漏的是遞迴。Claude Code 會對每一次巢狀自我衍生都套 launcher,所以一支會取得獨佔資源的 launcher,必須偵測自己已經持有它。你寫的那支腳本會呼叫到自己,這件事要當成前提,不是例外。

剩下的比較機械:

  • 不准重排、吞掉或前置參數。第一個參數是 Claude Code binary,後面全部是它的 argv。
  • 每次執行都要在大約三秒內走到 exec。一次冷啟動的背景派工會連續跑兩次 launcher 才吐出第一個位元組,SSO 交換這種慢工要延後做或走快取。
  • 在 Claude Code 啟動前不准對終端機輸出任何東西。exec 之前印的東西如果遇上 session 在初始化完成前死掉,會被當成當掉的原因回報。

設定值本身也有形狀。多數情況就是腳本的絕對路徑,要帶參數就寫在路徑後面,但 Claude Code 是把這個值當成參數陣列解析,不是 shell 指令。空白分隔 token,雙引號可以把含空白的內容組成一個 token;以 [ 開頭的值會被讀成 JSON 字串陣列,像 ["/opt/corp/launcher", "--profile", "cc"]。shell 語法在這裡一律無效:沒有變數展開、沒有 glob,未加引號的 ;|&$( 會被判為設定錯誤而拒絕,不會被重新解讀。

這篇我只讀了官方文件,沒有實際跑過任何一條指令,沒有寫過那支 launcher,也沒有在任何機隊上推過這個設定。上面所有指令、設定鍵、版本號都是照文件抄的。

另外兩件事文件沒寫,我就不猜:launcher 失敗時的實際錯誤訊息文字(文件只給了錯誤參考頁的連結),以及「大約三秒」這個時限超過之後究竟是拒絕啟動還是繼續等。

換掉的是入口,還是換掉了那份假設

以前做這件事,心智是找到那個入口、把它換掉。入口只有一個,換完就結案,剩下的交給報表。

這個變數逼你承認入口不只一個,而且有幾個你換不掉。文件交出來的東西比較像一張覆蓋率地圖:這六類我幫你包,那五項你自己想辦法,Windows 那一整排直接當成沒包。

我的立場是,把 CLAUDE_CODE_PROCESS_WRAPPER 當成合規的邊界會出事。它是覆蓋率工具,不是圍牆。交給稽核的東西不該只有一句「我們設了 launcher」,該是那五項加 Windows 機器的逐項處置:第二列在 PATH 放了什麼、deep link 有沒有用 disableDeepLinkRegistration 關掉、文件沒給補法的最後兩列你打算怎麼處理、那些 Windows 機器改用什麼手段補。

什麼情況下我會改口?如果哪天文件把前兩項收進包得到的清單,而且 Windows 有了等效的契約,那它就從覆蓋率工具變成邊界,上面這段話整段要重寫。在那之前,先把那張清單印出來貼在牆上,比設好變數重要。

原文來源:Corporate launcher - Claude Docs