有 Bug

删除是批准过的,路径却错了

The delete was approved. The path was wrong.

智能体本想清理临时或构建目录,但目标路径变成了空串、被引号拆开,或者被一条过宽的防护规则“放行”,结果删掉的是项目本身(或者它旁边的一切)。

适用于: Codex · Cursor · Claude Code

症状

在“清理临时目录”或“重建 venv”之后,下一条命令立刻报 “No such file or directory” 或 “not a git repository”。源码、`.git` 和备份都没了,日志里的命令目标是 `""`、某个盘的根目录或上级目录。

为什么会这样

智能体在多层 shell 之间(PowerShell → wsl → bash)用字符串拼路径,却没人检查最终解析出来的目标。Codex #55161(2026-10-18):本应作用于 /tmp 的 `python -m venv --clear` 在项目目录下以空目标 `""` 运行,清空了工作区。Cursor 论坛报告(经 NOPE 整理,2026-10-03):用户同意清理约 18.5 GB 编译临时文件,`rmdir /s /q` 的路径被拆开,源码、Git 和备份全没了。Sebastian Spicker(2026-10-05):一条防护被放宽为允许“仓库根目录之外”的任何路径,测试传入了上级目录,`rm` 删光了所有同级仓库。

示例

Codex

bash
# intended: python3 -m venv --clear /tmp/test-env
# cwd: /mnt/d/Opencode/M_easytier   (the project)
/usr/bin/python3.12 -m venv --clear --copies --system-site-packages ""
# next step: project test runner no longer exists

复制即用的修复

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

规则

Claude Code:加入 CLAUDE.md

CLAUDE.md
## Resolve and check every delete target
- Before any recursive delete or "clear" (`rm -r`, `rmdir /s`, `Remove-Item -Recurse`, `venv --clear`,
  `git clean`, `shutil.rmtree`), resolve the target to an absolute path and print it.
- Refuse if the target is empty, `/`, a drive root, `~`/home, the current working directory, the repo
  root, or any parent of the repo. Use `${VAR:?}` in shell so an empty variable aborts.
- Delete only inside an allowlist of disposable locations (a `mktemp -d` dir or a named build output).
  Never write guards as "anything outside X is allowed".
- Test destructive code with a temp directory or an injected delete function, never a real path.
- If a command fails with "No such file or directory" right after a delete, stop everything and tell the user.
规则

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

AGENTS.md
## Resolve and check every delete target
- Before any recursive delete or "clear" (`rm -r`, `rmdir /s`, `Remove-Item -Recurse`, `venv --clear`,
  `git clean`, `shutil.rmtree`), resolve the target to an absolute path and print it.
- Refuse if the target is empty, `/`, a drive root, `~`/home, the current working directory, the repo
  root, or any parent of the repo. Use `${VAR:?}` in shell so an empty variable aborts.
- Delete only inside an allowlist of disposable locations (a `mktemp -d` dir or a named build output).
  Never write guards as "anything outside X is allowed".
- Test destructive code with a temp directory or an injected delete function, never a real path.
- If a command fails with "No such file or directory" right after a delete, stop everything and tell the user.
Cursor 规则

Cursor:项目规则

.cursor/rules/delete-hit-the-wrong-path.mdc
---
description: "AgentFails: The delete was approved. The path was wrong."
alwaysApply: true
---

## Resolve and check every delete target
- Before any recursive delete or "clear" (`rm -r`, `rmdir /s`, `Remove-Item -Recurse`, `venv --clear`,
  `git clean`, `shutil.rmtree`), resolve the target to an absolute path and print it.
- Refuse if the target is empty, `/`, a drive root, `~`/home, the current working directory, the repo
  root, or any parent of the repo. Use `${VAR:?}` in shell so an empty variable aborts.
- Delete only inside an allowlist of disposable locations (a `mktemp -d` dir or a named build output).
  Never write guards as "anything outside X is allowed".
- Test destructive code with a temp directory or an injected delete function, never a real path.
- If a command fails with "No such file or directory" right after a delete, stop everything and tell the user.
钩子

PreToolUse 钩子:拒绝目标为空或危险的删除

用 matcher "Bash" 注册。它只检查命令文本;真正的保护是操作系统级沙箱加备份。

.claude/hooks/check_delete_target.py
#!/usr/bin/env python3
"""Block recursive deletes/clears whose target is empty, a root, home, or a parent dir."""
import json, re, sys

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

deletes = r"(\brm\s+-\S*r|\brmdir\s+/s|Remove-Item\b.*-Recurse|venv\s+.*--clear|shutil\.rmtree)"
bad_target = r"(\"\"|''|\s/\s*$|\s/[\s;&|]|\s~/?(\s|$)|\$HOME/?(\s|$|\")|\s\.\.(/|\s|$)|\s[A-Za-z]:\\?(\s|$|\"))"
if re.search(deletes, cmd) and re.search(bad_target, cmd):
    print("BLOCKED: delete target is empty, a root, home, or a parent directory. "
          "Resolve the absolute path, show it to the user, and ask.", file=sys.stderr)
    sys.exit(2)
if re.search(r"\brm\s+-\S*r\S*\s+\"?\$\{?\w+\}?/", cmd) and ":?" not in cmd:
    print("BLOCKED: use ${VAR:?} so an empty variable cannot expand to /.", 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)

来源与致谢

相关条目

每周翻车

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