作者报告 Claude Code 在开发删除保护时误删 Mac 主目录文件,并推测子 Agent 在保护生效前执行了传入真实主目录的测试。事件暴露出命令行检查难以覆盖脚本内部删除路径的问题,作者因此将主要防线转向操作系统沙箱与备份。
2026 年 10 月 7 日深夜,我让 Claude Code 处理项目时,Mac 主目录中的大量文件被删除了。
Claude Code 在执行命令前运行的检查,以及我自己编写的钩子,都只检查 Claude 输入的那一行命令。它们看不到这条命令所运行的脚本内部传给 rm -rf 的路径。因此,我现在的主要防线是沙箱和备份:沙箱在操作系统层面限制进程可以写入的位置。
关于我:我在一家 IT 公司担任 PM,并用 Claude Code 开发三款 iOS 应用。其中提供英文版的是 LifeCounter:每天一个问题,记录你与父母还能共度的时光。
原因(我目前最合理的推测):当时的任务是为一个脚本添加护栏,使它在参数为主目录时停止执行。一个子智能体在编写护栏之前就运行了测试,而测试把我真实的主目录传给了脚本。
损失:大约 9 天尚未推送的提交连同本地 .git 一起丢失。我从对话记录中恢复了 1,004 个文件,但子智能体写入的内容不在对话记录中,无法恢复。
主要防线:在操作系统层面限制写入位置的沙箱,以及保存在 Mac 之外的备份。拒绝规则和钩子是辅助防线。
首先要做的三件事(每一项的验证方法见“检查你自己的配置”):在 ~/.claude/settings.json 中设置 sandbox.enabled: true 和 allowUnsandboxedCommands: false。操作系统会限制命令可以写入的位置,Claude 也无法再到沙箱之外重试被阻止的命令。无法在沙箱内运行的命令,需要按名称列入 excludedCommands,才能在沙箱外运行。使用 Time Machine 或类似工具,将备份保存在 Mac 之外,并在批准提交后立即推送。即使在沙箱内,工作目录中的文件仍然可能被删除。删除逻辑的测试只能在每次测试专用的临时目录中运行。如果传入真实路径,被删除的就是真实文件。
在 ~/.claude/settings.json 中设置 sandbox.enabled: true 和 allowUnsandboxedCommands: false。操作系统会限制命令可以写入的位置,Claude 也无法再到沙箱之外重试被阻止的命令。无法在沙箱内运行的命令,需要按名称列入 excludedCommands,才能在沙箱外运行。
使用 Time Machine 或类似工具,将备份保存在 Mac 之外,并在批准提交后立即推送。即使在沙箱内,工作目录中的文件仍然可能被删除。
删除逻辑的测试只能在每次测试专用的临时目录中运行。如果传入真实路径,被删除的就是真实文件。
在沙箱外运行的内容:Xcode 测试和构建、xcrun simctl、gh,以及 git push。我尽量缩短这份清单,并为每一项记录理由和相应的补偿性防护措施。
配置、钩子和 68 项测试:claude-code-guards 仓库。
本文反映的是我截至 2026 年 10 月 19 日的配置(macOS、Claude Code CLI 2.1.292,以及 VS Code 扩展 2.1.301)。
当时,我让 Claude Code 在 auto 模式下通宵开发一款 iOS 应用。auto 模式会将每次操作的权限确认交给分类器(一个独立模型),而不是交给人来处理。
Me --> Main session --> Worker subagents x3 --> Review subagent
|
| passes
v
Next task
(no check by me)
我既没有配置沙箱,也没有设置 Time Machine。
发给子智能体的每条指令都包含“不要推送”。
sign_macos27.sh 是一个 shell 脚本,我用它为 Shortcuts 应用所用的文件签名并进行验证。它会对作为参数传入的目录执行 rm -rf。下表中的“RED”是指 TDD 中先运行测试、确认测试失败,再进行实现的步骤。
早上读到报告时,我脱口而出:“开什么玩笑!”大约 9 天没有推送到 GitHub 的提交,连同本地 .git 一起消失了。
子智能体的日志位于被删除的目录中,而且当晚还执行过一次 macOS 更新,因此我无法确定究竟是什么删除了这些文件。
执行命令前,Claude Code 会读取 Claude 输入的那一行命令,并决定允许还是阻止执行。记录中没有保留当晚的确切命令,但根据子智能体最后一条日志,我认为那是一条运行测试的命令。我推测,删除操作并未直接出现在那行命令中,而是发生在测试调用的脚本内部。
+------------------------------------------------------------------+
| What the pre-execution checks see: the one line Claude types |
| |
| Command that runs the tests (my guess) |
| - no rm, no home directory path in it |
+------------------------------------------------------------------+
|
v
Pre-execution checks
(deny rules / critical path protection / auto mode classifier)
|
| nothing dangerous in the text -> allowed (my guess)
v
+------------------------------------------------------------------+
| What they don't see: what that command runs |
| |
| Test runs in RED (guard not written yet) |
| -> passes the real home directory to sign_macos27.sh |
| -> rm -rf deletes the contents of the home directory <---+ |
+---------------------------------------------------------------|--+
|
Sandbox (not set up): if present, the OS would have denied ---+
writes outside the working directory |
Backup (none): if present, I could have restored from it -----+
上方方框是执行前检查能看到的内容,下方方框是它们看不到的内容,最底部的两层则是我当时没有配置的防线。主会话要求添加一道护栏,而子智能体最后一条日志是“添加护栏/选择测试并运行 RED”。我的理解是:添加护栏及其测试,然后运行 RED。我认为,这个测试原本是要确认 sign_macos27.sh 在接收到主目录时会停止执行。但在 RED 阶段,护栏尚未写好,因此我推测 sign_macos27.sh 对传入的真实主目录执行了 rm -rf。所有执行前检查能看到的,都只是一行“运行测试”的命令,应该没有理由阻止它。
官方文档也指出,Bash 规则检查的是 Claude 写出的那行命令:
Bash 规则匹配的是 Claude 写出的命令文本,Claude Code 会先拆分复合命令并剥离包装层。它不会匹配以其他形式调用的同一个程序,因此,deny 或 ask 规则覆盖的是 Claude 通常生成的调用形式,并不构成围绕该程序的安全边界。
《配置权限》——“Bash 规则无法匹配的内容”
命令执行前会运行三项检查。本文将它们统称为“执行前检查”。
如果 Claude 直接输入 rm -rf ~,关键路径保护应该会拒绝执行。我事后添加的钩子接收到的,也同样只是这一行命令。
在 auto 模式下,如果 rm 和 rmdir 的目标是关键路径,并且你使用的是终端,系统就会弹出确认提示;如果 2 分钟内没有回应,操作会被拒绝。在终端以外的环境中,操作会直接被拒绝,不会弹出提示(v2.1.281 及更高版本)。
当晚使用的版本是 2.1.286。记录显示,操作来自 iPhone、桌面应用和网页。我无法从记录中判断 Claude Code 的界面是否为终端,但无论是哪种情况,允许规则和钩子都无法放行这些操作。
文档中提到会进一步解析其内容的,仅限于子 shell 和 bash -c 之类的内联脚本,没有提到脚本文件。
没有沙箱时,你允许执行的命令可以写入当前用户账户有权写入的任何位置。沙箱由操作系统对运行中的进程强制实施,因此,无论模型运行什么,即使获准的命令实际执行的操作超出了其名称所暗示的范围,沙箱限制仍然有效(见文档)。所以,我把沙箱和备份作为主要防线,将拒绝规则和钩子作为辅助防线,用于尽早拦截常见的危险操作模式。
我从对话记录中恢复了 1,004 个被删除的文件,但子智能体写入的内容并不在这些记录中,因此,对话记录无法替代备份。
其他官方指导也指向同样的方向:
OpenAI 的 Codex 在 CLI 和 IDE 扩展中,默认也不允许网络访问,并将写入范围限制在工作区内。
OWASP LLM06:2025“过度代理权限”(Excessive Agency)也建议:对连接到 LLM 的工具和扩展,只授予完成任务所必需的最小权限。
完整配置位于 claude-code-guards 仓库。
~/.claude/settings.json(节选):
{
"sandbox": {
"enabled": true,
"allowUnsandboxedCommands": false,
"failIfUnavailable": true,
"excludedCommands": ["xcodebuild *", "xcrun simctl *", "gh *", "git push *", "Tools/test.sh", "Tools/test.sh *"],
"network": {
"allowLocalBinding": true,
"allowAllUnixSockets": true,
"allowMachLookup": ["*"]
},
"filesystem": {
"allowWrite": ["~/Library/Developer", "/opt/homebrew/share/flutter/bin/cache", "~/.pub-cache",
"~/.dartServer", "~/.dart-tool", "~/.flutter", "~/.config/flutter",
"~/.cocoapods", "~/Library/Caches/CocoaPods", "~/Library/Caches/com.charcoaldesign.swiftformat",
"~/Library/Caches/SwiftLint", "~/Library/Caches/org.swift.swiftpm", "~/Library/Logs/CoreSimulator"],
"denyRead": ["~/.ssh", "~/.aws", "~/**/.env", "~/**/*.p8"],
"denyWrite": ["~/**/Tools/test.sh", "~/**/.githooks",
"~/Projects/*/.git/hooks", "~/Projects/*/.git/config", "~/Projects/*/.claude"]
},
"credentials": {
"files": [
{ "path": "~/.ssh", "mode": "deny" },
{ "path": "~/.aws", "mode": "deny" },
{ "path": "~/.config/gh", "mode": "deny" },
{ "path": "~/.netrc", "mode": "deny" },
{ "path": "~/.git-credentials", "mode": "deny" },
{ "path": "~/.npmrc", "mode": "deny" }
],
"envVars": [{ "name": "ANTHROPIC_API_KEY", "mode": "deny" }]
}
}
}
我在 allowWrite 中添加了 Flutter、CocoaPods、SwiftLint 等工具写入缓存的位置。我没有通过 excludedCommands 把这些工具移到沙箱外运行,而是添加它们需要写入的位置,让它们可以在沙箱内运行。
这三项网络设置削弱了沙箱的隔离能力。我添加它们,是为了支持模拟器的 XPC 通信,以及 flutter test 在 localhost 上监听。allowAllUnixSockets 则是为了支持 Xcode 测试使用的套接字。收紧其中两项(allowMachLookup 和 allowAllUnixSockets)已列入我的“减少在沙箱外运行的内容”待办事项。官方文档指出,允许 Unix 套接字,以及允许向可执行文件所在位置写入,都可能成为逃逸沙箱的途径。
在 macOS 上,受管理的(管理员)设置放在 /Library/Application Support/ClaudeCode/managed-settings.json 中(需要 sudo)。
managed-settings.json:
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false
}
}
在 Linux 和 WSL2 上,包含 *、? 或 [ 的 allowWrite 和 denyWrite 条目不会生效,包括合并到沙箱设置中的 Edit(...) 规则。我只在 macOS 上验证过这套配置。
~/.claude/settings.json(节选):
{
"permissions": {
"deny": [
"Bash(git push --force*)", "Bash(git push -f*)", "Bash(git push * --force*)", "Bash(git push * -f*)",
"Bash(git push * +*)", "Bash(git push * :*)", "Bash(git push * -d *)",
"Bash(git push *--delete*)", "Bash(git push *--mirror*)",
"Bash(git reset --hard*)", "Bash(git clean -f*)", "Bash(git branch -D*)",
"Bash(gh alias *)", "Bash(gh extension *)", "Bash(gh repo delete*)", "Bash(gh release delete*)",
"Bash(gh repo edit*)", "Bash(gh gist *)", "Bash(gh secret set*)", "Bash(gh auth token*)",
"Bash(gh api *-X*)", "Bash(gh api *--method*)", "Bash(gh api *-f*)", "Bash(gh api *-F*)", "Bash(gh api *--input*)",
"Read(~/.ssh/**)", "Read(~/**/.env)",
"Edit(~/**/Tools/test.sh)", "Edit(~/**/.githooks/**)",
"Edit(~/.claude/hooks/**)", "Edit(~/.claude/settings.json)",
"mcp__xcode"
]
}
}
针对文件写入工具的规则写成 Edit(...)。它们适用于所有内置文件编辑工具,例如 Edit 和 Write。Read(...) 和 Edit(...) 的拒绝规则也会合并到沙箱设置中,因此同样适用于沙箱内的 Bash(但不适用于 excludedCommands 中的命令)。
禁止 Edit 修改钩子文件和 settings.json,可以防止 Claude 改写钩子。在沙箱外运行的 Xcode MCP 服务器,其所有工具都通过服务器名称被禁用了(我此前从未调用过它)。
这会阻止通过 gh 和 git push 删除或修改 GitHub 仓库及数据的主要操作;这两个命令都在沙箱外运行。像 gh * delete* 这样的宽泛模式,也会匹配 gh pr create 的正文内容,所以我改为逐一列出子命令。
~/.claude/settings.json(节选):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Monitor",
"hooks": [
{ "type": "command", "command": "/bin/bash", "args": ["/Users/YOU/.claude/hooks/guard-delete.sh"], "timeout": 10, "onFailure": "block" }
]
},
{
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
"hooks": [
{ "type": "command", "command": "/bin/bash", "args": ["/Users/YOU/.claude/hooks/guard-write.sh"], "timeout": 10, "onFailure": "block" }
]
}
]
}
}
警告:使用 args 时,钩子命令启动时不会经过 shell。~ 不会展开,在我的环境中,这导致钩子以退出码 127 退出。设置 onFailure: block 后,2.1.295 及后续版本会将其视为失败并阻止操作。在更早的版本中,127 不是用于阻止操作的退出码(2),因此命令仍会执行。请将 YOU 替换成你自己的用户名,并确认 grep -n '/Users/YOU' ~/.claude/settings.json 没有任何输出。
删除钩子(Bash、Monitor)
只允许在命令运行目录所属的 git 仓库内执行删除(仓库根目录本身和 .git 除外),以及在临时目录内执行删除。
对于 bash -c、eval、heredoc、| bash、python -c 等形式,会进一步解析其中的内容。对于 perl -e、node -e、ruby -e、awk、swift -e 等形式,则不会解析;如果其中包含 rmtree、rmSync、unlink 或 system 等与删除或移动有关的词,就会被阻止。
如果删除操作的目标路径不是以字符串字面量写出的,例如使用 os.path.join(os.sep, …) 或字符串拼接,就会被阻止。任何无法解析的内容,例如未知变量或 $(...),也会被阻止。符号链接按其实际指向的目标判断。
git push、xcodebuild、Tools/test.sh 等在沙箱外运行的命令,只允许在直接位于 ~/Projects 下的仓库中执行。
如果钩子本身在检查过程中崩溃,仍会以退出码 2 阻止操作(根据文档,退出码 1 不会阻止操作)。
如果钩子超时或启动失败,"onFailure": "block" 会阻止该操作。这项设置从 CLI 2.1.295 起生效;Homebrew 的 2.1.292 会将其视为未知键并忽略。
写入钩子(Edit、Write)
允许在 ~/Projects 下的 git 仓库内、临时目录内,以及少数特定位置写入,例如 Claude 的记忆文件和计划文件。
即使在这些位置,仍然禁止写入 .git,以及可能添加沙箱外执行内容的配置文件或目录(.claude/、.mcp.json、.githooks/ 等)。
我没有采用“只要存在 .git 就允许”的规则。/opt/homebrew 和 ~/.oh-my-zsh 也是 git 仓库,能够向这些位置写入,就意味着能够劫持 PATH。
我让一个审查子智能体检查了四轮钩子,每一轮都发现了绕过方式。我修复了所有问题,并将它们加入测试。下面三项是我从中挑出的例子,方便编写自己钩子的人在测试中检查这些情况。现在它们都已被阻止。
用引号拆分命令名,例如 "r""m" -rf ~/x。
将命令名放进变量,例如 $C -rf ~。
向临时目录中创建的仓库执行 git push,使目标仓库的钩子在沙箱外运行。
由于钩子在无法判断时一律阻止,因此存在误报。它们曾阻止过一些完全不执行删除的命令,例如一个用于编辑文章的 Python 脚本,只因为其中包含字符串 rm -rf ~;还有 grep 搜索词中的反引号。我把发现的每一种情况都加入了测试,并进行了修复。
通过重定向、cp、sed -i 或 truncate 覆盖文件属于正常编辑操作,因此钩子不会阻止这些操作。沙箱负责阻止仓库外的写入,而仓库内的内容则通过提交和推送来恢复。
完整的检查列表见 README。
测试会向判断函数传入一个假的主目录(/fake/home)。它们从不在临时目录之外执行任何删除操作。两个钩子共有 68 项测试。下面的节选省略了 ENV 和 fake_repo_of 的定义。
tests/test_guard_delete.py(节选):
HOME = "/fake/home"
PROJ = "/fake/home/proj"
TMP = "/fake/tmp"
def problems(cmd, cwd=PROJ):
return g.judge(cmd, cwd, None, HOME, [TMP], ENV, repo_of=fake_repo_of)
class BlocksOutside(unittest.TestCase):
def test_home_and_root(self):
for cmd in ["rm -rf ~", "rm -rf ~/Projects", "rm -rf $HOME/.claude", "rm -rf /",
"rm -rf ../other-project", "rmdir ~/Desktop", "unlink ~/.zshrc"]:
with self.subTest(cmd=cmd):
self.assertTrue(problems(cmd), cmd)
哪些内容在沙箱外运行
无法在沙箱内运行的命令,以及需要使用我已设为沙箱内不可读的凭据的命令,都列在 excludedCommands 中,并在沙箱外运行。前四行是我有意放在沙箱外运行的命令,每一项都有相应的补偿性控制措施。无论如何设置,MCP 服务器都会在沙箱外运行,因此我将它们列在最后一行。
这些调用都发生在沙箱外,因此也会经过 auto 模式分类器的评估(文档)。
官方文档对应该将哪些内容放入 excludedCommands 给出了警告:
被排除的命令会以你的完整访问权限运行。……如果你编写的匹配模式涵盖了解释器、工作目录中的脚本,……Claude 就可以写入该文件,然后在沙箱外运行它。
配置沙箱化的 Bash 工具
Claude 会编写 Xcode 测试代码,因此 xcodebuild 就属于这种情况。这也是为什么,除了上述控制措施之外,我还会持续推送代码并运行 Time Machine,确保任何被删除的内容都能恢复。
"onFailure": "block"。现在,即使钩子超时或启动失败,命令也会被阻止。这已在我平时使用的 VS Code 扩展(2.1.301)中生效。CLI 从 2.1.295 版本开始支持。我的权限模式仍然是 auto。沙箱内命令可写入的位置受到操作系统限制,因此默认会被允许执行,无须经过分类器(文档)。对于工作目录内的删除操作,钩子能提供帮助;即使仍有内容被删除,也可以通过已推送的代码和 Time Machine 恢复。只有表格中的项目会在沙箱外运行,所以我选择在这套配置下委托 Claude 执行,而不是停下来批准每一个操作。
针对第 4 项,下面是一种写法。${D:?} 会在变量为空时停止执行。在我的环境中,mktemp -d 曾经失败过一次,D 仍然为空,随后执行了 cd $D,结果进入了我的主目录(沙箱阻止了之后的写入操作)。
set -eu
D=$(mktemp -d)
cd "${D:?}"
据我判断,漏洞并不在 Claude 输入的那一行命令中,而在那条命令所运行的测试和脚本内部。只检查那一行命令无法深入到这一层,因此我主要依靠操作系统强制执行的沙箱和备份,并尽量减少在沙箱外运行的内容。
你采取了哪些措施,来防止 Claude Code 执行非你本意的破坏性操作?如果它真的执行了,你又准备如何应对?
我使用这套配置开发的 iOS 应用:LifeCounter(App Store)
设置、钩子和测试:ryodevapp/claude-code-guards
另一篇关于同一事件的文章,重点讨论心态而非设置(日语):note
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。