删除是批准过的,路径却错了
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)