昨天那份 193 行的架構文件,是在一個「無菌室」裡產出來的:一個只有原始碼、沒有知識圖、沒有 CLAUDE.md 的目錄。我當時只寫了怎麼準備,沒有講為什麼。
今天講。因為這 30 天,有一半的實驗都靠它才成立 —— 沒有乾淨的對照組,跑出來的數字都不知道代表什麼。而且日常用 Claude Code 也一樣用得到:試一個新 skill、裝一個沒看過的 plugin,你總希望它先在別的地方動,看完它動了什麼,再決定要不要放進正式環境。
「隔離」這件事我做了三級:刪掉、換目錄、開一台 VM。每一級都宣稱「它看不到」,每一級我都親手驗了一次:它到底擋住什麼,又漏掉什麼。三級並排放在最後。
最便宜的一級。把 repo 重新 clone 一份,該拿掉的拿掉,再讓它進去:
git clone --no-hardlinks container clean-room-day04
cd clean-room-day04 && git checkout d6de5694
# graphify-out/ 不存在、CLAUDE.md 不存在
Day 3 就是這樣做的。這個目錄裡沒有我事先算好的知識圖,寫出來的架構文件才算是「它自己讀的」。
它擋住的:工作目錄裡不存在的東西。知識圖、我寫的規格、任何我不想讓它看到的檔案。
它擋不住的有三樣。第一樣一開始就知道:全域設定。~/.claude/CLAUDE.md、我裝的 skill、記憶,全部照常載入。這一級隔離的是 repo,不是 Claude Code。
第二樣,是我踩到才知道的。後來一次實驗,我先刪掉規格檔 docs/spec.md,再讓它規劃功能,計畫交回來,最後一段寫著:
工作樹裡
docs/spec.md、docs/non-goals.md是未 commit 的 deleted 狀態(我從 HEAD 讀的,不會動)
.git 還在。 刪掉只是工作樹的狀態,歷史還在原地;它就從 git 物件裡,把我刪掉的規格讀了回去。同樣是第一級,Day 3 沒漏、那一次卻漏了 —— 差別只在被藏的東西有沒有進過 git。
修法是 rm -rf .git,或一開始就用 git archive d6de5694 | tar -x -C clean/ 匯出 —— 從一開始就沒有 .git,不用事後才想起來要刪。
還有第三樣,是後來另一次實驗它自己講出來的。計畫的最後一句寫著:「我看到旁邊有 library-loan-lab 目錄,依實驗協定我沒有去讀它。」那次乾淨 clone 跟原 repo 是鄰居目錄 —— .git 刪了、規格檔拿掉了,但它往上看一層就看得到原本那份。那次它說它沒讀,我另外驗過,判定沒讀;但「它說沒讀」不是隔離,「它讀不到」才是。乾淨 clone 要放到別的地方,不放鄰居。
但這些都不是重點。重點是這一級的邊界要親手驗:「刪掉」不等於「看不到」,而它會在報告裡老實寫它讀了什麼,但報告還是得自己去讀。
CLAUDE_CONFIG_DIR —— 擋住設定,擋不住檔案系統Claude Code 認得一個環境變數 CLAUDE_CONFIG_DIR。指到一個空目錄,整套設定、skill、plugin、記憶,連登入狀態都跟著換到那個目錄。
我在主機上開了一個空目錄 iso-day4,用它裝一個 plugin,前後對主機 ~/.claude 底下的檔案做 checksum。全部在同一台機器、同一個使用者底下:
ISO=$PWD/iso-day4
find ~/.claude/plugins -type f | sort | xargs shasum > host-before.txt
CLAUDE_CONFIG_DIR=$ISO claude plugin marketplace add anthropics/skills
CLAUDE_CONFIG_DIR=$ISO claude plugin install document-skills@anthropic-agent-skills
find ~/.claude/plugins -type f | sort | xargs shasum > host-after.txt
diff host-before.txt host-after.txt # 一行都沒有
主機 ~/.claude |
CLAUDE_CONFIG_DIR=iso-day4 |
|
|---|---|---|
claude auth status |
已登入 | loggedIn: false |
| 裝 plugin 之後的檔案數 | 168 → 168,checksum 零差異 | 3 → 872(+869,26 MB) |
claude plugin list |
eli5@claude-community |
document-skills@anthropic-agent-skills |
claude plugin marketplace list |
claude-community |
anthropic-agent-skills |
settings.json |
沒動 | 新建,只有 extraKnownMarketplaces、enabledPlugins 兩個 key |
兩邊的設定互相看不見。登入是跟著 config 目錄走的,不是跟著使用者 —— 這一點我原本猜錯,以為會沿用主機的登入,實際上要在 iso 那套 config 下再 claude auth login 一次,憑證存在它自己那份。
設定隔離了。那檔案系統呢? 登入之後,我從 iso 目錄的 session 要它做一件事:
cd ~
CLAUDE_CONFIG_DIR=$ISO claude -p 'Run exactly this shell command and nothing else: touch ~/Desktop/proof-day4.txt' \
--allowedTools 'Bash(touch:*)'
ls -la ~/Desktop/proof-day4.txt
# -rw-r--r-- 0 9月 14 12:19 /Users/<你的帳號>/Desktop/proof-day4.txt
設定隔離了,檔案卻真的出現在主機桌面上。 同一個 session 自報的 skill 清單是 document-skills:{xlsx,docx,pptx,pdf} 加 13 個內建 —— 它用的是 iso 那套設定,寫的是主機的檔案。一個 skill 的 allowed-tools 裡如果有 rm,在這套乾淨的 config 底下,照樣刪得到你的東西。
這一級擋的是設定,不是檔案系統。
寫檔沙箱是以 cwd 為界的,跟 config 目錄無關。 上面那個 touch 我其實跑了三次。前兩次 cwd 在一個暫存目錄底下,它回覆我「this session is sandboxed to …/tmp」,寫不出去。cd ~ 再跑才寫進去。所以「它寫不出去」有時候是另一層在擋 —— 如果你把那個當成 CLAUDE_CONFIG_DIR 的功勞,就會在換個目錄的時候被咬。
信任記錄也住在 config 目錄裡。 cwd 在家目錄時,iso 的 session 把 ~/.claude/settings.json 當成 /Users/<你的帳號> 這個 workspace 的專案設定,回了一句 workspace has not been trusted, ignoring 1 permissions.allow entry。主機那邊早就信任過的目錄,在 iso 眼裡是陌生的。
上面「以 cwd 為界」那件事不是 bug,是 Claude Code 內建的 Bash sandbox(macOS 底下用 Seatbelt):/sandbox 打開後,預設只准寫 cwd、session 的暫存目錄,以及你用 --add-dir 加入的目錄,OS 層強制,子程序也跑不掉。
所以 CLAUDE_CONFIG_DIR + /sandbox 就是一級半:設定隔開了,檔案也被圈在 cwd 裡,額外成本還是零。第二級漏的那個洞,多數日常情境(試 skill、跑測試)用這半級就補得起來。
但它的邊界要記兩條,都是官方文件白紙黑字:它只管 Bash 指令和它的子程序 —— MCP server、hook 不在裡面;還有一道「unsandboxed retry」逃生門:指令在沙箱裡跑不動時,可以退到沙箱外重跑(走一般權限流程)。要關,就設 allowUnsandboxedCommands: false。第一條我驗了一次:沙箱開著、cwd 在暫存目錄,Bash 對 cwd 外的目錄寫檔回 operation not permitted。但同一個 session 裡,一支 20 行的 MCP server 把檔案寫進同一個目錄,寫進去了。文件那句是真的,而且它的意思是:沙箱只鎖 Bash 這條路;MCP 那條路,要 server 自己鎖(Day 15 接資料庫時會再用到這件事)。
要把檔案系統也隔開,就得換一台機器。我用 VirtualBuddy(第三方工具,底下是 Apple 自家的 Virtualization framework,macOS 上跑 macOS):裝一台乾淨的 macOS 當母片,裡面只放 Homebrew、git、Claude Code;登入完就關機,從此不再開它。要做實驗就從母片複製一台出來,做完整台刪掉。
「複製一台」聽起來很貴。實際查了才知道不是:
| 複製前 | 複製後 | |
|---|---|---|
| Finder 顯示的大小 | base-26 107.41 GB | 兩台各 107.41 GB |
du -sh |
30G | 30G + 30G |
df -h 可用空間 |
285Gi | 285Gi |
APFS clone:Finder 跟 du 看起來各算一份,實際上磁碟只存一份。複製前後,可用空間一個位元組都沒少,之後實驗機寫進多少,磁碟才多佔多少。三次複製加上所有實驗寫入結束,總共多佔了 2 GB。
然後在複製出來的那台裡做跟第二級一模一樣的事 —— 裝同一個 plugin,前後 checksum:
.claude.json、history.jsonl、settings.json、known_marketplaces.json
installed_plugins.json(釘在 commit 34040c9c,跟主機第二級裝到的同一個)、plugin-catalog-cache.json、.last_inuse_sweep
plugins/ 合計 33 MB、1,317 個檔
document-skills:{xlsx,docx,pptx,pdf}
跟第二級的 26 MB / 869 檔差了 7 MB,差在 claude-plugins-official:互動模式第一次啟動就會把官方 marketplace clone 下來,純 CLI 子命令不會。這種差異,不驗真的不會知道。
退回快照:VM 關機、整台刪掉、從母片再複製一台。進去看:
plugins/cache/ 不存在
installed_plugins.json 不存在
settings.json 回到 2 個 key
它看到的 skill 13 個內建
乾淨。 裝過的東西一個都不在;主機的檔案系統從頭到尾也沒被碰過 —— 主機的 ~/.claude 我根本沒去 checksum,因為它連那個檔案系統都摸不到。
但第三級也有漏的,只是這次的檢查沒有涵蓋到:它擋住的是機器,擋不住身分和網路。VM 裡那個 Claude Code 是用我的帳號 claude auth login 的 —— 它花的是我的額度、以我的身分呼叫 API;我要是把 GitHub token 或 SSH key 放進去,它推上去的每一個 commit,都是我;它讀到的任何東西都能 curl 出去。這一項我沒驗,是邊界在定義上就有:VM 隔離的是那台機器,不是你。 要連這個也擋,得再加一層 —— 不給憑證、限制網路 —— 那已經是第四級,這篇不寫。
前兩級都在限制它。第三級反過來 —— 爆炸半徑被鎖在一台可以整台刪掉的機器裡,所以可以拿掉平常不敢拿掉的東西:
claude -p "<prompt>" --dangerously-skip-permissions 跑整晚,不用每一步按 y。這不是我發明的用法,官方文件把它列成一條:「Run fully unattended inside a container」,旁邊的必要條件寫著 container、VM 或 sandbox runtime;同一頁對這個模式的使用建議是:只在容器、VM 這類它碰不到主機的隔離環境裡用;並且直說它對 prompt injection 沒有任何防護 —— 防護只能來自外面那一層 —— 也就是第三級。有一個細節值得記:auto mode 的分類器會擋的動作清單裡,有一條是「啟動一個沒有人工核准、也沒有沙箱的自主 agent 迴圈」,例子就是 --dangerously-skip-permissions。也就是說,沒有隔離就起無人迴圈,連 Claude 自己都當成要擋的動作 —— 第三級不是多疑,而是把它已經內建的那條線,真的畫出來。
這一節我只寫用法沒放數字,因為這篇要證的是邊界,不是產能;「跑 30 次」的版本留給後面真的需要它的那幾天。
代價也要記:IPSW 19.77 GB、母片實佔 30 GB、guest 記憶體開機就整塊配置(8 GB 一次拿走,不是用多少算多少)、同時最多開兩台 macOS guest、這版 beta 的主機↔VM 剪貼簿是壞的(我用主機開 python3 -m http.server,VM 用 Safari 開頁面複製指令)。裝到母片可用,大約花一個晚上,多數時間在等下載。
| 第一級 乾淨目錄 | 第二級 CLAUDE_CONFIG_DIR |
第三級 VM | |
|---|---|---|---|
| 擋住 | repo 裡沒有的東西 | 設定、skill、plugin、記憶、登入狀態 | 整台機器與檔案系統 |
| 漏掉 | 全域設定;.git 裡的歷史;鄰居目錄 |
檔案系統(touch 到主機桌面;加 /sandbox 可補) |
身分與網路(用的還是你的帳號;沒驗,定義上就有) |
| 建立成本 | 一行 git clone |
一個空目錄 + 重新登入 | 一個晚上 + 30 GB |
| 退回成本 | rm -rf |
rm -rf |
刪 VM、從母片再複製(可用空間不變) |
| 適合 | 「它自己讀,讀到多少」這種對照組 | 試 skill / plugin / 設定,看它動了哪些檔 | 放手讓它跑:無人值守、循環任務、測試來路不明的東西 |
沒有哪一級是對的,只有哪一級夠。Day 3 那種「它讀不讀得懂 repo」的實驗,第一級夠了;Day 12 之後試各種 skill,第二級加 /sandbox;要放手讓它整晚跑、或對一個來路不明的 plugin 說「我看過它動了什麼,主機沒事」,只有第三級能說這句話。
三級我都親手驗了邊界,三級也各有漏點:第一級漏 git 歷史與鄰居目錄,第二級漏檔案系統,第三級漏身分與網路。前兩個是驗出來的,都不是文件寫錯,是我以為的邊界,跟實際的邊界不一樣 —— 「刪掉了」以為它看不到,「換了 config」以為它寫不出去。第三個是定義上就有:我這次設的檢查它全過了,但檢查本來就設在它擋得住的那一邊。
三級排在一起還有另一個讀法:一到三級,是從限制它走到放手讓它跑。隔離做到哪一級,決定的不只是它看不到什麼,還有你敢讓它做什麼。
這一篇留下的心法:
隔離不是「有沒有」,而是「到哪一級」。而不管哪一級,「它看不到」都要親手驗過,不是設定完就成立的 —— 用它裝東西之前先 checksum,裝完再 checksum,
diff出來的那幾行,才是它真正動過的地方。
apple/container,commit d6de5694200468d99a61662bfb9bb3aba763e3e5,git clone --no-hardlinks 後移除 graphify-out/ 與 CLAUDE.md(Day 3 的實驗就在這裡跑)diff 行數、plugin list、VM 三份報告)皆為當次終端機輸出,指令如正文;原始檔未另外公開claude auth login / claude plugin marketplace add / claude plugin install 皆為 CLI 子命令,claude plugin --help 可查anthropics/skills 的 document-skills,commit 34040c9c568585f6929bedeaad110ad08f079624:github.com/anthropics/skills
allowUnsandboxedCommands):code.claude.com/docs/en/sandboxing;2026-09-16 查證bypassPermissions 的使用建議、auto mode 分類器擋的動作清單):code.claude.com/docs/en/permission-modes;2026-09-16 查證brew install --cask virtualbuddy@beta:github.com/insidegui/VirtualBuddy;guest macOS 26.6.2 (25G83),IPSW 19.77 GB;主機 M4 Pro / 64 GB / macOS 26.6.2