📦 不一定。以 Claude Code 為例,開了 sandbox 之後 Bash 只能寫專案目錄、連不到沒允許的網址,但家目錄的憑證檔和環境變數裡的密鑰照樣讀得到;要另外擋讀取、拿掉環境變數裡的密鑰、設定 sandbox 起不來就停,再實際跑一次確認。
昨天在 Day 20|寫在 Prompt 裡的規則,擋得住 Agent 嗎?,我們看了 gemini-cli 的 Issue 分類 workflow:prompt 裡的守則擋不住,要把 token 拿出模型那一步,寫回交給模型外的程式;用 Claude Code 的話,規則寫進權限設定,要擋檔案和網路就開 sandbox。今天接著看:sandbox 是什麼,開了到底擋住了什麼?
快速回顧一下:Day 20 提過,Claude Code 的權限規則 Read(./.env) 擋得住 cat,擋不住一支自己去開檔案的 Python 腳本,因為規則比對的是工具呼叫寫了什麼;sandbox 換成由作業系統來擋,管的是 Bash 和它啟動的 subprocess。
2025 年 9 月,npm 上出現一隻叫 Shai-Hulud 的蠕蟲。被植入的套件在安裝時跑一段 postinstall 腳本(npm 裝完套件會自動執行的指令),把環境變數整份倒出來,用 TruffleHog 掃檔案找金鑰,再把結果送到 webhook.site 和一個新建的公開 GitHub repo;接著用偷到的 npm token,把自己發到那位維護者的其他套件裡(StepSecurity 的分析)。
假設你在 Claude Code 裡請它幫忙裝依賴、跑測試,而且已經用 /sandbox 開了 sandbox。依賴裡只要有一個被植入的套件,那段安裝腳本就會在 sandbox 裡跑。它碰得到什麼?我照 Shai-Hulud 會做的事挑了五個動作,用假的憑證檔和假的環境變數,請 Claude Code 在 sandbox 裡一個一個跑。
開著預設的 sandbox,家目錄裡的假憑證,cat 讀得到,Python 也讀得到;環境變數裡的假 token 印得出來。寫家目錄和連外面的網址,都被擋下來。

圖:左邊是 Shai-Hulud 這類安裝腳本會做的事 → 中間是 Claude Code 預設 sandbox 的實測結果,讀家目錄的假憑證、印環境變數都成功,寫家目錄和連 example.com 被擋 → 右邊是 sandbox 管不到的地方:Claude 內建的讀檔工具、hooks、MCP server、你自己打的 ! 指令、excludedCommands、不開 sandbox 的重跑,還有 sandbox 起不來時照常執行。這是示意,實作要照自己的工具和環境調整。
sandbox 擋住了寫和連外,讀和環境變數沒擋。下面先講 sandbox 是什麼、各家怎麼用、底下靠什麼做到,再把實驗的設定和改前改後的結果並排,接著看還有哪些東西本來就在 sandbox 外面,最後落成啟動 sandbox 時最少要做的檢查。
sandbox 指的是把一段程式關在劃好範圍的環境裡跑:它能讀寫哪些檔案、能連哪些網址,由環境外面的東西決定,程式自己改不了。範圍畫在哪一層,常見的做法有三種:用作業系統的機制限制單一個 process、放進 container、放進有自己 kernel 的 VM,後面「底下靠什麼做到」那節會分開講。
Claude Code 在你電腦上的 sandbox 用的是第一種。開了之後,模型提出的 Bash 指令不會直接交給 shell 跑。Claude Code 先用作業系統提供的機制,替這次要啟動的 shell 套上一份規則:哪些路徑能讀、哪些能寫、網路只能連到哪裡。規則套在 process 上,它啟動的 subprocess 全部跟著繼承。之後 process 每次開檔、寫檔、連線,都由作業系統照規則檢查,不在範圍內就失敗。
所以 sandbox 不看指令怎麼寫。cat 讀檔和 Python 自己開檔,在作業系統那一層都是同一個開檔動作。這是它跟 Day 20 的權限規則差最多的地方,兩個並排看:
| 權限規則 | Claude Code 的 sandbox | |
|---|---|---|
| 誰檢查 | Claude Code,在工具執行之前 | 作業系統,在 process 開檔、寫檔、連線的時候 |
| 看什麼 | 工具呼叫寫了什麼:指令文字、路徑參數 | process 實際要碰的檔案和網址 |
| 管到哪些工具 | 全部,包括內建的 Read、Edit、WebFetch 和 MCP 工具 | 只有 Bash 指令和它啟動的 subprocess |
| 擋不住 | 換一種寫法,例如 grep -r、自己開檔的 Python |
不經過 Bash 的東西:內建工具、hooks、MCP server |
兩樣要一起用:權限規則決定哪些工具呼叫要先問你,sandbox 限制 Bash 指令跑起來之後碰得到什麼。
Anthropic 介紹 Claude Code sandbox 的文章講了兩個用途。第一個是防 prompt injection(藏在網頁、Issue 這類外部內容裡、想讓模型照著做的指示):模型就算被騙了,檔案隔離讓它改不到系統上的敏感檔案,網路隔離讓它送不出資料、也下載不了惡意程式。文章也講了為什麼兩樣都要開:少了網路隔離,被騙的 Agent 可以把 SSH key 這類檔案送出去;少了檔案隔離,它很容易就逃出 sandbox、拿到網路。
第二個用途是少問你幾次。範圍內的指令可以直接跑,不用每個都等你按同意;Anthropic 說他們內部使用時,跳出來要你確認的次數少了 84%。
Claude Code 的 sandbox 預設是關的,要用 /sandbox 或在設定裡寫 sandbox.enabled: true 打開。
上一節是照隔離做在哪一層來分;換成照「關進去的是什麼」來分,各家的用法有兩種:一種只把 Agent 執行的指令關起來,Agent 本身還是跑在你的電腦上;另一種把整個 Agent 放進 container 或 VM。本機的 coding agent 多半是前一種,雲端版本是後一種。
Claude Code 兩種都有。在你電腦上跑的,就是上一節的 sandbox,只包 shell 指令;網頁版的 Claude Code 每個 session 跑在 Anthropic 管理的 VM 裡,git 的憑證不放進 VM,由 VM 外面的 proxy 在送出時附上。Claude Code 的執行環境比較也寫了,只開 Bash 的 sandbox 不夠讓 Agent 完全沒人看著跑;要用 --dangerously-skip-permissions 跳過所有確認,就放進 container、VM,或 Anthropic 開源的 sandbox-runtime(把 Claude Code 用的同一套限制拿來包整個程式)裡。
其他幾家各一句:
.git 和 .codex 還是受保護。Codex 的雲端版本跑在 OpenAI 管理的 container 裡,裝環境的階段可以上網,Agent 開始工作後預設離線。--yolo(全部自動同意)時才預設打開。macOS 上的預設規則只把寫入限制在專案目錄,讀檔和網路都放寬;也可以改成整個跑在 Docker、Podman 或 gVisor 裡。.git/config、.git/hooks、.vscode 另外保護;網路先全擋,再照你的允許清單和 Cursor 內建的套件管理器網域打開。放不進 sandbox 的指令,交給另一個模型判斷要不要放行。雲端的 Agent 各自跑在一台專用的機器上。放在一起看,寫入範圍和網路各家都收,讀這一塊 Claude Code 和 Gemini CLI 的預設都放得很寬,家目錄讀得到。OpenAI 談內部 Codex 部署的文章把可寫範圍、受保護路徑和網路政策分開管理,不認識的網域進核准流程,憑證放在作業系統的 Keyring。這是他們自己的部署方式,我會借的是那個拆法:讀、寫、網路、憑證,自己的設定裡各設到哪裡,一項一項看。
一個 Bash 指令在 Claude Code 的 sandbox 裡是這樣走的:
模型提出一個 Bash 指令
→ Claude Code 套上規則,再啟動 shell
macOS:Seatbelt(sandbox-exec 帶一份規則檔)
Linux:bubblewrap(給 process 自己的檔案系統視角和網路)
→ 指令和它的 subprocess 開檔、寫檔
→ 作業系統照規則檢查,不在範圍內:Operation not permitted
→ 指令要連網路
→ 只通得到 sandbox 外面的 proxy
→ proxy 比對網域清單,不在清單上:403
macOS 上用的是 Seatbelt,macOS 內建的 process 限制機制:用 sandbox-exec 啟動程式時帶一份規則檔,寫明哪些路徑能讀寫、能不能連網路。Claude Code 背後的 sandbox-runtime 每次照設定產生這份規則檔;Codex、Cursor、Gemini CLI 在 macOS 上也都是用 Seatbelt。
Linux 上 Claude Code 和 Codex 預設用 bubblewrap。它用的是 Linux 的 namespace,這個功能可以讓一個 process 看到另一套檔案目錄和網路設定:bubblewrap 只把允許的目錄放進那套檔案目錄,網路那一份則是空的,沒有能連到外面的網卡。Cursor 在 Linux 上改用 kernel 內建的 Landlock 限制檔案存取,再用 seccomp 擋掉特定的 system call(程式向作業系統要求開檔、連線這類動作的入口)。
網路那一段,sandbox 裡的指令沒有直接對外的路,只連得到 sandbox 外面的一個 proxy(Claude Code 透過 Unix socket 接過去)。proxy 看每個連線要去的網域,在清單上才放行。下一節實驗裡的 Operation not permitted 和 403,就是這兩道檢查各擋下來的。proxy 預設只看網域名稱、不看加密連線的內容,文件也提醒這可能被 domain fronting 繞過,也就是連線表面上寫允許的網域,實際送到別的主機。
把整個 Agent 關起來的那一種,差在跟主機共用多少:
| 做法 | 跟主機共用什麼 | 例子 |
|---|---|---|
| container | 同一個 kernel,靠上面講的 namespace 分開 | OpenHands 的 Docker sandbox、Gemini CLI 的 Docker 選項、Codex 雲端 |
| gVisor | 另外跑一個用一般程式寫成的 kernel,裡面的程式發出的 system call 先被它接住,不直接送到主機的 kernel | Gemini CLI 的 runsc 選項 |
| microVM、VM | 各有自己的 kernel,靠 hypervisor(管虛擬機的那一層)分開 | E2B 每個 sandbox 一台 Firecracker microVM、Docker Sandboxes、網頁版 Claude Code |
差別在 kernel。container 裡的程式跟主機用同一個 kernel,kernel 一旦有漏洞,裡面的程式就有機會碰到主機;gVisor 和 VM 多隔了一層,裡面的程式碰不到主機的 kernel。越往下隔得越開,代價是比較吃資源。
不管哪一層,你把主機上的資料夾接進去(也就是掛載),裡面的程式就看得到:OpenHands 的 Docker 設定可以用 SANDBOX_VOLUMES 把主機路徑掛進 container,掛成可寫,Agent 就改得動。掛進去的每一個路徑,都要用下一節的五個指令問一次。
實驗環境是 macOS、Claude Code 2.1.285,模型是 Claude Opus 5.5。家目錄放一個仿 AWS 憑證檔的假檔案 ~/.day21-fake-credentials,啟動 Claude Code 時帶一個假的環境變數 DAY21_FAKE_TOKEN=fake-day21-token。用 claude -p 交代它照順序執行五個指令,每個指令單獨呼叫一次 Bash,指令照抄,被擋就記下錯誤、不要改寫,也不要要求在 sandbox 外重跑。
設定分兩份,放在兩個空資料夾的專案設定裡。一份只寫 "sandbox": { "enabled": true },另一份收緊:
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"filesystem": {
"denyRead": ["~/"],
"allowRead": ["."]
},
"credentials": {
"envVars": [
{ "name": "DAY21_FAKE_TOKEN", "mode": "deny" }
]
}
}
}
四個設定各管一件事,寫法都照文件:
denyRead 加 allowRead:擋掉整個家目錄的讀取,只留專案目錄。. 放在專案設定裡才會解析成專案根目錄。credentials.envVars 的 deny:每個 sandbox 裡的指令執行前,先把這個環境變數拿掉。文件也寫了,沒有內建的憑證清單,沒列出來的不會被保護。allowUnsandboxedCommands: false:關掉「在 sandbox 裡失敗,就請你同意改在外面重跑」這條路。failIfUnavailable: true:sandbox 起不來的時候,Claude Code 直接停,不改用沒隔離的方式跑。| 指令 | 想模擬的事 | 只開 sandbox | 收緊後 |
|---|---|---|---|
cat ~/.day21-fake-credentials |
掃家目錄的憑證檔 | 印出整份內容 aws_secret_access_key = fake-day21-secret-not-a-real-key |
cat: ...: Operation not permitted |
python3 -c "...open('~/.day21-fake-credentials')..." |
安裝腳本自己開檔 | 同上,讀到整份內容 | PermissionError: [Errno 1] Operation not permitted |
echo "token=$DAY21_FAKE_TOKEN" |
倒出環境變數 | token=fake-day21-token |
token= |
touch ~/sandbox-probe-day21 |
改家目錄的檔案 | Operation not permitted |
Operation not permitted |
curl ... https://example.com |
送到外部網址 | CONNECT tunnel failed, response 403 |
同左 |
只開 sandbox 的設定跑了 2 次,兩次的工具呼叫和輸出完全一樣。收緊的設定也跑了 2 次,第 2 次模型那一輪回覆被 Opus 5.5 自己的安全檢查攔下,五個指令都沒跑,表上是第 1 次的結果。
只開 sandbox 的結果,跟 Claude Code 的 sandbox 文件列的預設對得上:寫只限專案目錄、暫存目錄和另外加進來的目錄;讀是「機器上大部分地方,包括 ~/.ssh 和 ~/.aws/credentials 這類憑證檔」;環境變數從 Claude Code 繼承,「包括裡面的任何密鑰」;網路的允許清單一開始是空的。
放回 Shai-Hulud:改家目錄的檔案會失敗,送到 webhook.site 也會被 proxy 擋下,但它要偷的東西,環境變數和家目錄的憑證檔,都已經拿在手上了。允許清單也不會一直空著:要裝依賴,就得放行 npm 的套件庫 registry.npmjs.org,而 Shai-Hulud 拿偷到的 token 發布新版本,連的也是這個網域。文件對 github.com 有同樣的警告,加進清單可能變成外送的路,Shai-Hulud 偏偏就是把偷到的東西放進一個公開的 GitHub repo。
sandbox 只包 shell 指令和它們啟動的 process。下表前三列會讓指令整個跑到 sandbox 外面,安裝腳本也跟著出去;第四列是放進來的東西本身權限太大;後三列從頭就不經過 Bash:
| 情況 | 結果 |
|---|---|
指令符合 excludedCommands |
整個在 sandbox 外跑,例如把 docker * 排除,等於 docker 能做的事都放行 |
| 指令在 sandbox 裡失敗 | Claude 可以要求不開 sandbox 重跑;一般模式會先問你,bypassPermissions(跳過所有確認)模式不問 |
| sandbox 起不來,例如 Linux 少了 bubblewrap | 預設照常執行、不隔離;設了 failIfUnavailable 才會停 |
允許了 /var/run/docker.sock 這類 Unix socket |
文件的例子:等於拿到整台主機 |
| Claude 用內建的 Read、Edit、WebFetch 工具 | 不經過 sandbox,照權限規則走;denyRead 擋不住 Read 工具,要另外寫 Day 20 那種 Read(...) 規則 |
| hooks、本機的 MCP server、狀態列指令 | 用你完整的權限執行 |
你在 ! 後面自己打的指令 |
多數情況不在 sandbox 裡,所以自己打的指令測不出 sandbox 有沒有作用 |
Docker socket 那一列的原因,Docker 的文件講得很直接:能對 Docker daemon 下指令,就可能拿到主機的 root 權限。sandbox 擋得住檔案和網路,擋不住一個被你放進來、本身權限就很高的服務。
文件在限制那一節也寫了,sandbox 會降低風險,但不是完整的隔離邊界。要把上面這些都關進同一個邊界,就是前面講的另一種做法:把整個 Claude Code 放進 container 或 VM 裡跑。
今天的實驗本身就是最小的檢查:sandbox 啟動之後、交給 Agent 之前,先跑幾個「要被擋下來」的動作,有一個沒擋住就不開始。
| 欄位 | 這個情境的值或用途 | 誰讀寫 |
|---|---|---|
| config | 收緊後那份設定:擋家目錄讀取、拿掉 DAY21_FAKE_TOKEN |
維運的人寫,launcher(Harness 裡負責把執行環境開起來的那段程式)讀 |
| probes | 讀家目錄的假憑證、印受保護的環境變數、寫家目錄、連沒允許的網址,每一項都要失敗 | 維運的人寫,launcher 啟動後執行 |
| sandbox_id | 這次執行環境的編號 | 寫進 Run 紀錄,結束時用來清理 |
def start_sandbox(config, probes, launch, run_probe):
sandbox = launch(config)
if sandbox is None:
return {"status": "blocked", "reason": "sandbox_unavailable"}
leaked = [p.name for p in probes if run_probe(sandbox, p) == "allowed"]
if leaked:
sandbox.destroy()
return {"status": "blocked", "reason": leaked}
return {"status": "ready", "sandbox_id": sandbox.id}
| 誰呼叫 | 什麼時候 | 結果 |
|---|---|---|
| Harness 的 launcher | 每次要交給 Agent 跑指令之前 | 四個 probe 都被擋,回 ready |
| 同一個 launcher | sandbox 起不來,例如少了 bubblewrap | 回 blocked,不改用沒隔離的方式跑 |
| 同一個 launcher | 讀家目錄的 probe 讀到了內容,像今天的預設設定 | 銷毀環境、回 blocked,列出漏掉的項目 |

圖:左邊是改前、改後同樣五個指令的實測輸出,改前讀得到假憑證和假 token,改後五個都被擋 → 右邊是收緊的四個設定各管什麼 → 下方是 launcher 的檢查:sandbox 起不來就停,probe 有一項沒擋住就銷毀環境,都擋住才交給 Agent。這是示意,實作要照自己的工具和環境調整。
假設你從 Day 1 開始自己刻 Harness,已經做到 Day 20:會寫回外部服務的動作,已經搬到模型外面。這時模型提出的 shell 指令,多半是 Harness 直接開一個 subprocess 去跑,用的是你自己的帳號和權限。下一步是在這一行外面包一層 sandbox。
第一步,把執行指令的那一行換成包過的版本。 Seatbelt 規則檔和 bubblewrap 參數不用自己從頭寫。Anthropic 把 Claude Code 用的那一套開源成 sandbox-runtime,可以當指令列工具 srt 用,也可以當函式庫:Harness 啟動時呼叫 SandboxManager.initialize(config) 把網路 proxy 起起來,每次要跑指令就用 wrapWithSandbox(cmd) 拿到包好的指令再執行。被擋下的動作可以用 annotateStderrWithSandboxFailures 標進錯誤輸出,README 建議把工具呼叫的 ID 一起帶進去,模型和 log 都看得出是哪一次呼叫被擋。它還標著 research preview,設定格式可能會變。想看 Codex 的規則會怎麼處理某個指令,可以用 codex sandbox -- <指令> 直接試。
第二步,照讀、寫、網路、憑證四樣訂規則,拿 Codex 的預設當起點。 Codex 在 macOS 上的規則檔開頭就是全部拒絕,再一條一條打開;Linux 上先把整個檔案系統掛成唯讀,只把工作目錄掛成可寫,再把工作目錄裡的 .git、.codex 重新掛回唯讀;網路預設關閉。.git 要另外擋,是因為 .git/hooks 裡的腳本會在你下次 commit 時自動執行,那時已經不在 sandbox 裡,Agent 只要能改它,就等於留了一段之後在外面跑的程式。換成 sandbox-runtime 的設定,就是 allowWrite 只放工作目錄、denyWrite 放 .git、denyRead 擋家目錄,allowedDomains 從空的開始加,文件寫明空清單等於不能連網。
憑證要另外處理,sandbox 擋得住讀檔,擋不住你自己傳進去的環境變數。啟動 subprocess 時只傳指令需要的環境變數,密鑰不放進去。真的要帶憑證連外的,例如 git push,可以學 Cloudflare 的做法和 Docker Sandboxes:sandbox 裡只放一個假的值,請求離開 sandbox 之後,由外面的 proxy 換成真的憑證,Agent 從頭到尾拿不到。網頁版 Claude Code 的 git 也是這樣,真的 token 由 proxy 附上。
第三步,啟動時先檢查,起不來就停。 上一節的 start_sandbox 就是這一步。OpenAI 在 Codex Windows sandbox 的設計文章寫過一次失敗:第一版只靠環境變數叫程式走 proxy 來擋網路,他們自己形容這種保護只是「advisory」,不理會那些變數的程式照樣連得出去,最後改成給 sandbox 專屬的使用者帳號,再用防火牆規則擋。所以檢查用的指令要故意不照規矩走,例如 curl --noproxy '*' 不理會 proxy 設定直接連,才測得出這種漏洞。
第四步,要跑別人的程式碼,就換成每個任務一台用完就丟的 microVM。 跑陌生的 repo、多人共用,或 Harness 本身就在雲端,與其在本機一層一層補,不如每個任務開一台新的:E2B、Vercel Sandbox、Cloudflare Sandbox SDK 都是有自己 kernel 的 microVM;Modal 預設用 gVisor;自己有 Kubernetes 的,Agent Sandbox 讓你像建一個 Kubernetes 資源那樣開 sandbox,底層的隔離交給 gVisor 或 Kata Containers。換了這一層,第二步的問題還是要回答:掛進去哪些路徑、網路開到哪、憑證放不放進去。
用 Claude Code 的話,包指令和網路 proxy 它都做好了,要自己做的是把規則收緊、把退路關掉:用 denyRead 或 credentials 把家目錄和密鑰收起來(內建的 Read 工具另外照 Day 20 寫 Read(...) 規則),設 allowUnsandboxedCommands: false 和 failIfUnavailable: true(/sandbox 的 Overrides 分頁叫 Strict sandbox mode),再請 Claude 跑一次今天的五個指令確認。不要自己在 ! 後面打,自己打的多半不在 sandbox 裡。
只在自己的機器上改自己的專案,做到第二步就能擋掉 Shai-Hulud 這類腳本最想拿的東西;要跑陌生的 repo、在 CI 或多人共用的機器上跑,再做第四步。Day 22 看 Agent 做事時該用誰的身分、拿什麼憑證,Day 24 再看不可信的內容怎麼走到資料出口。
回到裝依賴的那個例子,可以試著問自己:開了 sandbox,安裝腳本讀得到家目錄的憑證檔嗎?如果這台機器的 sandbox 根本起不來,Claude Code 會怎樣?
第一題,預設讀得到。實測用 cat 和 Python 都讀到假憑證的整份內容,環境變數裡的假 token 也印得出來;要加 denyRead 或 credentials 才擋得住。第二題,預設照常執行、不隔離;設了 failIfUnavailable 才會停下來。不一定。以 Claude Code 為例,開了 sandbox 之後 Bash 只能寫專案目錄、連不到沒允許的網址,但家目錄的憑證檔和環境變數裡的密鑰照樣讀得到;要另外擋讀取、拿掉環境變數裡的密鑰、設定 sandbox 起不來就停,再實際跑一次確認。
昨天我們把寫回外部的動作搬出模型那一步,今天把模型那一步讀得到的東西收起來。接下來還有另一個問題:Agent 總要用某個身分去呼叫外部服務,它手上那把憑證,能開的門會不會太多、有效期會不會太久?
webhook.site 和新建的公開 GitHub repo,再用偷到的 npm token 發到其他套件。denyRead/credentials 的寫法、allowUnsandboxedCommands、failIfUnavailable、在 sandbox 外面的工具和指令,以及 github.com、Docker socket 的限制說明。實驗結果是我在 macOS、Claude Code 2.1.285 上跑的,換平台要自己重測。.git 和 .codex 受保護、macOS 用 Seatbelt、雲端版跑在 container 裡。--yolo 時預設打開。srt 或函式庫(SandboxManager)給自己的 Harness 用,設定裡的 filesystem 和 network 欄位、空的 allowedDomains 等於不能連網;目前標成 research preview。.git 和 .codex 重新掛回唯讀,舊的 Landlock 做法標成 legacy;指令列入口是 codex sandbox。SANDBOX_VOLUMES 怎麼掛進主機路徑,掛成可寫的影響。