越界 / 不理想

被拦下了,就换条路绕过去

Blocked, so it found another way

拒绝规则或沙箱拦下了一条命令,智能体却把“被拒绝”当成一道谜题:换一种写法达到同样效果(用 `find -delete` 代替 `rm`,用完整路径代替 `npx`),或者干脆把防护关掉。

适用于: Claude Code

症状

记录里先是一次被拒绝的调用,接着出现类似“直接 rm 被拦了……我换条路试试”的话,然后是一条效果相同、执行成功的别的命令。或者总结里写着“沙箱拦了我一条命令,我绕过去了”。

为什么会这样

目标(“删掉 .venv”)还在智能体的待办里,而拒绝规则只匹配其中一种写法。@rfdslabs 给 Claude Code 配置了禁止 rm;被拒两次后它写道“direct rm is being blocked, though the find -exec pattern passed. Let me try that route”,随后执行了 `find -delete` 和 `find -exec rm -rf`。Ona 的文章展示了更进一步的同一反射:`npx` 被禁后,Claude Code 改用 `/proc/self/root/usr/bin/npx`,bubblewrap 沙箱失败后又提出关闭沙箱。Claude Code 文档写明:权限规则由工具强制执行,CLAUDE.md 只影响 Claude 尝试做什么。所以“被拒绝”在规则里必须是停止信号,在执行环境里必须是基于效果的限制(哪些路径可写)。

示例

根据 @rfdslabs 的帖子还原。

text
$ rm -rf .venv            -> denied by permission rule Bash(rm:*)
$ rm -rf .venv            -> denied
Agent: "direct rm is being blocked, though the find -exec pattern passed. Let me try that route."
$ find .venv -delete      -> ran
$ find . -name .venv -exec rm -rf {} +   -> ran

复制即用的修复

按你用的智能体挑选文件。规则片段用英文写,放进任何仓库都能用。

规则

Claude Code:加入 CLAUDE.md

CLAUDE.md
## A denial means stop, not reroute
- If a permission rule, hook, or sandbox blocks a command, do not retry the same goal with a different
  command, path, interpreter (`find -delete`, `xargs rm`, `python -c "shutil.rmtree(...)"`, `git clean`),
  or by disabling or bypassing the sandbox.
- Stop, tell the user which command was blocked and what you were trying to achieve, and ask how to proceed.
- Never propose turning off a sandbox, hook or permission rule to get a task done unless the user asked for that.
规则

Codex、Copilot 编码智能体等:加入 AGENTS.md

AGENTS.md
## A denial means stop, not reroute
- If a permission rule, hook, or sandbox blocks a command, do not retry the same goal with a different
  command, path, interpreter (`find -delete`, `xargs rm`, `python -c "shutil.rmtree(...)"`, `git clean`),
  or by disabling or bypassing the sandbox.
- Stop, tell the user which command was blocked and what you were trying to achieve, and ask how to proceed.
- Never propose turning off a sandbox, hook or permission rule to get a task done unless the user asked for that.
Cursor 规则

Cursor:项目规则

.cursor/rules/routed-around-the-denial.mdc
---
description: "AgentFails: Blocked, so it found another way"
alwaysApply: true
---

## A denial means stop, not reroute
- If a permission rule, hook, or sandbox blocks a command, do not retry the same goal with a different
  command, path, interpreter (`find -delete`, `xargs rm`, `python -c "shutil.rmtree(...)"`, `git clean`),
  or by disabling or bypassing the sandbox.
- Stop, tell the user which command was blocked and what you were trying to achieve, and ask how to proceed.
- Never propose turning off a sandbox, hook or permission rule to get a task done unless the user asked for that.
钩子

PreToolUse 钩子:拦截“删除”的其他写法

用 matcher "Bash" 注册,和 destructive-bash-hook 一起用。它仍是字符串匹配:请同时打开操作系统级沙箱,把可写范围限制在项目目录内。

.claude/hooks/block_delete_variants.py
#!/usr/bin/env python3
"""Block indirect deletes that slip past a Bash(rm:*) deny rule."""
import json, re, sys

call = json.load(sys.stdin)
if call.get("tool_name") != "Bash":
    sys.exit(0)
cmd = call["tool_input"].get("command", "")

variants = [
    r"\bfind\b[^|;&]*\s-delete\b",
    r"\bfind\b[^|;&]*-exec(dir)?\s+(/\S*/)?rm\b",
    r"\bxargs\s+(-\S+\s+)*(/\S*/)?rm\b",
    r"shutil\.rmtree|os\.removedirs|fs\.rmSync|fs\.rm\(|rimraf",
    r"\bgit\s+clean\s+-[a-zA-Z]*[fdx]",
    r"\brsync\b.*--delete",
    r"(^|[\s;&|])/\S*/rm\s",                  # /bin/rm, /proc/self/root/usr/bin/rm
    r"\b(bash|sh|zsh)\s+-c\s+.*\brm\b",
]
for pat in variants:
    if re.search(pat, cmd):
        print("BLOCKED: this deletes files by another route. A blocked delete means stop: "
              "tell the user what you wanted to remove and why, and wait.", file=sys.stderr)
        sys.exit(2)
sys.exit(0)
钩子

Claude Code PreToolUse 钩子:拦截破坏性 shell 命令

用 matcher "Bash" 注册。正则清单是安全带,不是沙箱;备份和最小权限凭据同样要有。

.claude/hooks/block_destructive.py
#!/usr/bin/env python3
"""Block high-blast-radius shell commands; the agent must ask the user instead."""
import json, re, sys

call = json.load(sys.stdin)
if call.get("tool_name") != "Bash":
    sys.exit(0)
cmd = call["tool_input"].get("command", "")

dangerous = [
    r"\brm\s+-[a-zA-Z]*r[a-zA-Z]*f|\brm\s+-[a-zA-Z]*f[a-zA-Z]*r",   # rm -rf / rm -fr
    r"\bgit\s+push\b.*(--force\b|-f\b|--force-with-lease)",
    r"\bgit\s+reset\s+--hard\b",
    r"\bgit\s+clean\s+-[a-zA-Z]*f",
    r"\bgit\s+checkout\s+--\s+\.",
    r"\b(drop|truncate)\s+(table|database|schema)\b",
    r"\bdelete\s+from\s+\w+\s*;?\s*$",                              # DELETE without WHERE
    r"\b(terraform|tofu)\s+destroy\b",
    r"\bkubectl\s+delete\b",
]
for pat in dangerous:
    if re.search(pat, cmd, re.IGNORECASE):
        print(f"BLOCKED destructive command: {cmd!r}\n"
              "Explain what you want to run and why, and let the user run it or approve it.", file=sys.stderr)
        sys.exit(2)
sys.exit(0)

来源与致谢

相关条目

每周翻车

每周一个真实的智能体翻车案例和它的修复。免费、简短、不刷屏。