文章分析正常清理、空变量展开和外部指令注入三类删除场景,说明匹配字面文本 rm -rf 无法形成可靠防护。作者提出用测试展示规则失效位置,并讨论叠加防护措施。
如果你允许 coding Agent 执行 shell 命令,它迟早会想删除一些东西。通常这没什么问题,比如重新构建前清空 dist/。但偶尔,它会在变量为空时执行 rm -rf "$BUILD_DIR/",或者照着一份本不该信任的 README 执行清理步骤。这篇文章讨论的是:如何在真正需要拦截的场景下阻止 AI Agent 执行 rm -rf,同时让它在正常场景下依然能干活。
核心教训是:查找字面文本 rm -rf 的规则,只是一根绊线,算不上护栏。我会结合测试输出,具体展示它在哪些地方会失效,以及应该在它周围叠加哪些防护层。
rm -rf build 也是完全合理的执行方式。rm -rf "$OUT_DIR/" 本来没问题,但如果 OUT_DIR 为空,命令就会变成 rm -rf "/"。Agent 拼接命令很快,却很少加上 set -u 防护。禁止删除解决不了第一种情况,要求模型小心也解决不了第三种情况。你需要多层防护,而且这些防护不能依赖模型的判断。
在设置任何规则之前,先限制错误删除操作能波及的范围:
git status 干净,工作区里的大多数删除操作都能通过 git checkout . 恢复。只有这一层完全不受命令语法影响。后面的每一层,都是为了更早识别操作意图。
大多数 coding Agent 都允许你按命令模式设置拒绝规则。例如,在 Claude Code 中:
{
"permissions": {
"deny": ["Bash(rm -rf *)", "Bash(rm -fr *)"]
}
}
Gemini CLI 等工具也有类似机制。应该用起来,但也要清楚它们的局限。旧版 Gemini CLI 文档说得很直白:基于简单字符串匹配、针对特定命令的限制“很容易被绕过”。任何前缀或子串匹配规则都存在同样的问题。
下面这份策略使用 Cirvix policy DSL 编写,包含一条针对字面字符串的拒绝规则,以及一条宽松的 shell 放行规则。其中,command = "rm -rf" 会被编译为 contains 匹配:
deny:
name = deny-destructive-shell
tool = shell.exec
command = "rm -rf"
allow:
name = allow-shell
tool = shell.exec
test "rm -rf":
tool = shell.exec
command = rm -rf ./build
expect deny
test "rm -fr":
tool = shell.exec
command = rm -fr ./build
expect deny
test "rm -r -f":
tool = shell.exec
command = rm -r -f ./build
expect deny
test "find delete":
tool = shell.exec
command = find . -delete
expect deny
对这份策略运行 cirvix policy test,使用的软件包版本为 0.3.0:
✓ rm -rf → deny (deny-destructive-shell)
✗ rm -fr line 15
expected deny
actual allow by allow-shell
call shell.exec risk CRITICAL
✗ rm -r -f line 19
expected deny
actual allow by allow-shell
call shell.exec risk CRITICAL
✗ find delete line 23
expected deny
actual allow by allow-shell
call shell.exec risk HIGH
3 failed 1 passed
拦住了一种写法,漏掉了三种。不过,请留意 risk 列:引擎已经将 rm -fr 和 rm -r -f 归类为 CRITICAL,将 find -delete 归类为 HIGH。只是这条字面匹配规则根本没有查询风险等级。
解决办法是根据命令实际执行什么操作来编写规则,而不是根据它怎么写;同时,绝不能让通配放行规则覆盖危险类别:
deny:
name = deny-rm-rf-literal
tool = shell.exec
command = "rm -rf"
deny:
name = deny-critical-shell
tool = shell.exec
risk >= CRITICAL
reason = "A CRITICAL command must be permitted by a rule that names it, never by a wildcard."
require_approval:
name = hold-high-risk-shell
tool = shell.exec
risk >= HIGH
approvers = developer
allow:
name = allow-safe-shell
tool = shell.exec
risk <= MEDIUM
使用同类测试用例,再加上几个更贴近实际的场景:
✓ rm -rf → deny (deny-rm-rf-literal)
✓ rm -fr → deny (deny-critical-shell)
✓ rm -r -f → deny (deny-critical-shell)
✓ rm with empty variable → deny (deny-rm-rf-literal)
✓ find -delete → require_approval (hold-high-risk-shell)
✓ python rmtree → require_approval (hold-high-risk-shell)
✓ git clean → require_approval (hold-high-risk-shell)
✓ npm test → allow (allow-safe-shell)
8/8 PASSED
find -delete、python -c "shutil.rmtree(...)"、git clean -fdx,都会暂停执行,交由人工审批,而不是悄悄放行。对于“我无法判断具体行为的任意执行”,这才是正确的默认处理方式。npm test 位于一份简短、带锚定匹配的允许列表中,不会被打断。分类器刻意采用规则,而不是模型:同一条命令每次都会得到相同的风险等级。只要命令包含串联或替换字符,如 ;、&&、|、$(),就会失去进入安全允许列表的资格,因此 npm test; rm -rf ~ 无法借着 npm test 混进来。
无论使用什么引擎,都应该维护一份危险命令写法的列表,并在 CI 中运行测试。可以从下面这些开始:
rm -rf ./build rm -fr ./build rm -r -f dist
rm -rf "$EMPTY/" find . -delete git clean -fdx
python3 -c "import shutil; shutil.rmtree('x')"
xargs rm -rf < list.txt npm test; rm -rf ~
如果某条新规则错误地放行了其中一条命令,你会在 pull request 阶段发现,而不是等到事故发生后才知道。
策略只能看到经过它的调用。如果 Agent 能通过其他方式启动 shell,或者运行一份你从未检查过内容的脚本,策略评估的只是外层命令,例如 ./scripts/clean.sh。按照上面的规则,这条命令会因无法识别而被暂缓执行。这是一个合理的默认处理方式,但并不意味着策略能看到脚本内部。第 1 层防护仍然必须保留。
Cirvix AgentControl 是一个开源策略引擎,用于治理 AI Agent 的工具调用:它可以通过 Claude Code hook 管理 shell 命令,通过 gateway 管理 MCP 调用,也可以管理使用其 Node/Python SDK 封装的函数。上面的策略可以通过以下命令验证并运行:
npm install -g @cirvix_ai/agent-control
cirvix policy check --policy shell.policy
cirvix policy test --policy shell.policy
仓库中的 policies/default.policy 提供了一份更完整的基线策略,覆盖破坏性 shell 操作、历史记录重写、软件包安装和生产部署,并附有对应的测试用例。Cirvix 无法阻止 prompt injection,也只能治理经过它的调用;它提供的是命令执行前经过测试、可解释的决策。
仓库:CIRVIX/agent-control。文档与指南:Cirvix 官网。
如果需要采取进一步措施,可以考虑屏蔽此人和/或举报滥用行为。