Day 25 講了好幾個 Claude 一起做事怎麼不互相踩到。今天換一個問題:就算規矩寫得再清楚,總有 session 會做錯,這時候傷害能不能有上限?上限不能靠提示詞要它小心,要靠 Claude Code 自己或作業系統去執行的邊界。
Claude Code 有兩層這種邊界:權限規則和 sandbox。兩層常被當成同一件事,其實擋的東西完全不同。分不清楚,就會出現「我明明 deny 了,它還是寫進去」的狀況。Day 22 看過一個例子:在 acceptEdits 下只禁編輯工具,Bash 重導照樣能寫檔。今天把這件事講透。
| 權限規則 | sandbox | |
|---|---|---|
| 管誰 | 工具呼叫:Bash、Read、Edit、WebFetch、MCP 都算 | 行程:Bash、PowerShell、Monitor 的指令與它們的子行程 |
| 比對依據 | 指令的文字、路徑、網域 | 作業系統層實際的檔案與網路存取 |
| 什麼時候擋 | 指令執行之前 | 指令執行當中 |
| 預設狀態 | 沒設規則時依 mode 決定問不問 | 預設關閉,要自己打開 |
兩層就是這樣切的:權限控制哪些工具能用、能碰哪些檔案或網域;sandbox 是作業系統層的強制執行,限制 shell 指令的檔案系統與網路存取。強度也不同。權限決策在指令跑之前,依指令字串做出;sandbox 的邊界由作業系統套在執行中的行程上,不管模型選了什麼指令,也不管一個被允許的指令實際做的比名字看起來更多。
與其問哪一層比較強,不如先問:你要擋的這件事,是看得出來的文字,還是看不出來的行為?前者權限規則夠用,後者要靠 sandbox。


規則由 Claude Code 執行,不是模型。你在提示詞或 CLAUDE.md 裡寫的話會影響 Claude 想做什麼,但不會改變 Claude Code 允許什麼。這是為什麼「一定不能做」的事要寫成規則,不能只寫成叮嚀。
規則依序評審:deny、ask、allow,第一個命中的決定結果,規則寫得多具體不會改變順序。這帶來兩個直接的後果:
Bash(aws *))會擋掉所有命中的呼叫,包括同時命中窄 allow(Bash(aws s3 ls))的那種。allow 不能在 deny 上開例外。組織想要硬保證的事,放在 managed settings 的 deny,這是優先序最高的一層,下面的人改不了。
Bash 規則比對的是 Claude 寫出來的指令文字,在拆開複合指令、剝掉固定的包裝指令(例如 timeout、nohup)之後。複合指令中,deny 與 ask 只要任何一個子指令命中就生效,包括藏在子 shell 或迴圈裡的;allow 則要每個子指令各自被允許。
但它對「同一個程式用不同形式呼叫」無能為力。常見的對照:
| 規則 | 擋得到 | 擋不到 |
|---|---|---|
Bash(curl *) |
curl https://example.com |
/usr/bin/curl https://example.com、sh -c 'curl ...' |
Bash(rm *) |
rm -rf build/ |
/bin/rm -rf build/、bash -c 'rm -rf build/' |
Bash(git push *) |
git push origin main |
git -C . push origin main、git 'push' origin main |
另外,devbox run、npx、docker exec 這類執行器不會被剝掉,規則會比對到它後面的一切。想用規則去限制指令的參數也很脆弱,改個旗標、換成 https、先指派變數再用,都能繞過。
所以結論是:deny 或 ask 規則涵蓋的是 Claude 通常會產生的那種呼叫,不是圍住那個程式的安全邊界。要不依賴指令文字就能擋,用 sandbox;要用自己的邏輯檢查完整指令,用 PreToolUse hook。
這兩條規則不只管內建的檔案工具,也管 Claude Code 認得的檔案指令(cat、head、tail、sed、tee)和 Bash 重導向的目標。但不管這種情況:指令沒指名檔案就去讀(例如在含有該檔的目錄執行 grep -r),或是任意子行程間接讀寫檔案(例如一支自己開檔的 Python 或 Node 腳本)。要擋掉所有行程碰某個路徑,要開 sandbox。
permission mode 決定哪些動作不問就做:default(介面上叫 Manual,default 是設定值)只放行讀取;acceptEdits 再放行工作目錄內的檔案編輯與常見的檔案系統指令;auto 由另一個分類器模型審規則沒涵蓋、也不是一般讀寫的動作;dontAsk 只做讀取與預先核准的,其餘一律拒絕;bypassPermissions 什麼都不問。
幾個和邊界直接相關的事實:
bypassPermissions(Day 22 提過)。反過來,allow 在 bypassPermissions 裡沒有作用,因為本來就全放行了。Bash(*)、Bash(python*) 這類能執行任意程式的 allow。離開 auto 後才恢復。permissions.deny,它在分類器看到之前就擋了。bypassPermissions 對提示注入和意外動作沒有任何保護。 只該在容器、VM 或 dev container 這種隔離環境、而且沒有對外網路的情況下使用。PreToolUse hook 在權限提示之前執行,exit 2 會擋下工具呼叫(Day 17 講過),而且在規則被評估之前就擋。hook 也可以替沒有規則命中的呼叫省掉提示。但它回傳 allow 並不能蓋掉規則:命中的 deny 規則照擋,命中的 ask 規則照問。
sandbox 預設關閉,用 /sandbox 或設定打開。它建立在開源的 @anthropic-ai/sandbox-runtime 上,macOS 用 Seatbelt,Linux 與 WSL2 用 bubblewrap。原生 Windows 不支援,指令會在沒有 sandbox 的情況下直接跑。
還有一個很容易漏掉的預設:sandbox 啟動不了的時候,預設是繼續跑,不是停下來。 依賴沒裝、平台不支援,Claude Code 都會照樣執行指令,只是不套 sandbox。想讓它啟動失敗就停,要設 sandbox.failIfUnavailable: true。
打開之後的預設邊界:
| 存取 | 預設 |
|---|---|
| 寫入 | 工作目錄、使用者暫存目錄、你額外加入的目錄;受保護路徑維持禁寫 |
| 讀取 | 整台機器的大部分,包括 ~/.ssh 與 ~/.aws/credentials 這類憑證檔 |
| 網路 | 沒有直接的出路,連線都走本機的 proxy,由它逐個 host 比對放行名單,名單一開始是空的 |
| 環境變數 | 從 Claude Code 繼承,包括裡面的任何機密 |
預設擋寫,不擋讀。 這點最容易被誤會成「sandbox 讓行程看不到機密」。要擋讀取,得自己列出 filesystem.denyRead 或設定憑證保護。也沒有內建的憑證 deny 清單,只有你列出來的檔案與變數才會被限制。
限制掛在作業系統層,所以 sh -c、Python 的 open()、subprocess 這些走不同路徑的寫法,通通受同一套邊界約束。這正是上一節規則比對不到的那塊。寫入被擋的時候,行程看到的是一般的權限錯誤,macOS 是 Operation not permitted,Linux 是 Read-only file system。網路被擋時,Claude Code 會在結果裡寫出被拒絕的 host,讓 Claude 看得見是哪一個。
網路方面,直連沒有路,所以不吃 proxy 環境變數的工具(直接執行的 ssh、多數資料庫驅動)連放行名單內的主機也連不上,UDP、QUIC、ping 同樣出不去。名單外的 host 怎麼處理,要看 mode:default 與 acceptEdits 會問你,auto 拒絕(除非指令自己列出 host 並經分類器核准),dontAsk 拒絕,bypassPermissions 放行。想要一律拒絕、不問,設 strictAllowlist,它只認使用者層、managed settings 與 --settings。
sandbox 只包住 shell 指令。Claude 的檔案工具也在它外面,但由權限規則管;command hook、本機 MCP server、LSP、statusline、apiKeyHelper 則以完整權限在跑。想全包住,要把整個 Claude Code 行程放進 sandbox runtime、容器或 VM。
也就是說,開了 sandbox 之後,Read 工具照樣讀得到 ~/.ssh,要擋它還是得靠 Read 的 deny 規則。
兩層不是各管各的,有一組設計好的整合:
| 權限規則 | 在 sandbox 裡的效果 |
|---|---|
Edit allow |
等同 sandbox.filesystem.allowWrite |
Read、Edit deny |
併入 denyRead、denyWrite |
WebFetch(domain:...) |
併入網域的允許與拒絕清單 |
所以路徑型的 Edit(./config/**) deny,既管檔案工具,也管 sandbox 內的 Bash。這是 Day 22 那個問題的另一種解法:只禁 Edit、Write 這兩個工具名擋不住 Bash 寫檔,要擋就用路徑型的規則,或直接設 sandbox.filesystem.denyWrite。就算沒開 sandbox,路徑型的 Edit deny 也擋得到 > file 這種重導,擋不到的是腳本自己開檔。
反向不成立:denyRead 擋不了 Read 工具,allowedDomains 也限制不了 WebFetch。
sandbox.autoAllowBashIfSandboxed 預設是 true:指令在 sandbox 內跑,就自動核准,不問。效果是:在 default 模式下,檔案編輯工具會問你,但同一個檔案用 sandbox 內的 Bash 寫就不問。
這個設計是為了減少提示,合理,但要知道它改變了你的使用體驗:問的次數少了,邊界全部落在 sandbox 本身設得對不對。deny 規則在 auto-allow 下仍然有效,有內容的 ask 規則(例如 Bash(git push *))也仍然會問。這兩層的交界處出過 bug:有版本修過「環境變數前綴放在指令前面時,deny 或 ask 規則在 auto-allow 下被漏掉」。所以版本要保持更新。
指令在 sandbox 內失敗時,Claude 會分析失敗原因,可能用 dangerouslyDisableSandbox 參數重試。誰來核准取決於 mode:default 與 acceptEdits 會問你,auto 由分類器判,dontAsk 拒絕,bypassPermissions 不問。
要留意兩件事:
Bash(curl *) 的 allow,它也會放行那次在 sandbox 外執行的 curl,沒有提示。Bash(dangerouslyDisableSandbox:true),它的優先序高於命中的 allow。想直接堵死,設 sandbox.allowUnsandboxedCommands: false,Claude Code 就會忽略這個參數。excludedCommands 是另一個口:列在裡面的指令完全在 sandbox 外跑,沒有檔案限制、沒有 proxy。它跑的是你的完整權限。寫成涵蓋直譯器、工作目錄內的腳本、或會依檔案行動的工具(例如 docker compose 讀它的 compose 檔),Claude 就可能先寫那個檔,再在 sandbox 外執行它。
proxy 依客戶端提供的 hostname 決定放行,預設不檢查 TLS。放行 github.com 這種大網域,可能留下資料外洩的路徑;sandbox 內的程式也可能用 domain fronting 這類手法連到名單外的 host。有效的 sandbox 同時需要檔案系統與網路隔離,缺一個,就能外洩或植入後門。

多 session 協作、背景 session、無頭執行,共同點是沒有人在每個提示前按確認。這時候邊界要先設好,因為提示不會替你擋。
permissionMode 時繼承父對話的 mode。 父對話是 bypassPermissions、acceptEdits 或 auto 時,子代理一律跑一樣的 mode,忽略它自己設定的 permissionMode。sandbox 也是同一套設定。dontAsk 除外)。lead 用 --dangerously-skip-permissions,全員都是,這是 Day 25 講過的。別假設每個 teammate 各自在 sandbox 裡,需要隔離就把整個行程放進容器。對無人值守的建議很明確:sandboxed Bash 只管 shell 指令,單獨用它不足以支撐完全無人值守的執行;auto 的分類器是逐個動作的控制,不是隔離邊界,所以隔離邊界仍然是額外的縱深防禦;而 --dangerously-skip-permissions 則必須搭配容器、VM 或 sandbox runtime 這種隔離環境。
下面是一份中性的示意設定,放在專案的 .claude/settings.json,讓 repo 裡的規則跟著專案走。它示範的是分層的想法,不是一份該照抄的清單:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./secrets/**)",
"Edit(./config/prod/**)"
],
"ask": [
"Bash(git push *)"
]
},
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"filesystem": {
"denyRead": ["~/.aws"]
},
"network": {
"allowedDomains": ["registry.npmjs.org", "github.com"]
}
}
}
這份示意裡每一段都對應前面講的一個縫:
permissions.deny 用路徑型規則:擋檔案工具,同時併進 sandbox 的 denyRead 與 denyWrite。代價是 sandbox 內的 Bash,包括 dev server 與測試,也讀不到這些檔,需要它們的指令要改由你在外面跑,或換成不寫進專案的機密注入方式。ask 放對外動作:git push 這類有內容的 ask 規則,在 auto-allow 下仍然會問。但它只攔得到平常寫法的 git push,換成 git -C . push 就繞過去了。網域名單裡有 git host,sandbox 就不會攔推送,所以兩層要一起看。failIfUnavailable:避免 sandbox 起不來時照樣執行指令。要注意它的代價:在原生 Windows,或沒裝 bubblewrap 與 socat 的 Linux 上,Claude Code 會直接起不來。放進專案設定,就會套用到每個開這個 repo 的人,成員環境不一致時,改放 .claude/settings.local.json 或各自的使用者層。allowUnsandboxedCommands: false:堵死逃生口,Claude Code 會忽略 dangerouslyDisableSandbox。想保留逃生口、但每次都問,改成加一條 Bash(dangerouslyDisableSandbox:true) 的 ask 規則。兩種擇一,不必同時用。denyRead 補預設的讀取缺口:預設整台機器都讀得到。要不要連 ~/.ssh 一起擋要看情況,在 Linux 與 WSL2 上,擋了之後 SSH 協定的 git remote 會讀不到金鑰。git push 走 HTTPS 時要連 git host,所以名單裡要有它,這就是名單放得進大網域的代價。macOS 上 SSH remote 在 sandbox 內不通,就算 host 在名單裡也一樣,要改用 HTTPS remote,或用 excludedCommands 把 git 的網路指令拿出來,但那就是前面講的另一個口。想讓名單外一律直接拒、不問,可以設 strictAllowlist,但它只認使用者層、managed settings 與 --settings,開了之後 repo 裡的 allowedDomains 會被忽略,名單要跟著搬到同一層。選哪一層,可以這樣判斷:
| 你要擋的是 | 用什麼 |
|---|---|
一個看得出來的固定動作(git push、讀 .env) |
權限規則 |
| 「不管用什麼程式,都不能碰這個路徑」 | 路徑型的 Read/Edit deny,加上開啟 sandbox(deny 會併進去) |
| 完整指令文字要依自己的邏輯檢查 | PreToolUse hook |
| 一定不能發生,組織層級保證 | managed settings 的 deny 與 sandbox 鎖 |
| 無人看管、要整個行程圈起來 | 容器、VM 或 sandbox runtime,不只是 sandboxed Bash |

bypassPermissions。
Edit、Read 規則會併進 sandbox。反向不成立,denyRead 擋不了 Read 工具。excludedCommands;網路只看 hostname。