The delete was approved. The path was wrong.
删除是批准过的,路径却错了
The agent meant to clear a temp or build directory, but the target came out empty, split by quoting, or "allowed" by a guard that was too loose, so the delete hit the project (or everything next to it).
Applies to: Codex · Cursor · Claude Code
Symptom
Right after a "clean up the temp dir" or "fresh venv" step, the next command fails with "No such file or directory" or "not a git repository". Source files, `.git` and backups are gone, and the command in the log targets `""`, a drive root, or a parent directory.
Why it happens
Agents build paths in strings across shells (PowerShell → wsl → bash), and nobody checks the final resolved target. Codex #55161 (18 Oct 2026): a `python -m venv --clear` meant for /tmp ran with an empty target `""` from the project directory and cleared the workspace. A Cursor forum report (via NOPE, 3 Oct 2026): the user agreed to clear about 18.5 GB of build temporaries, a `rmdir /s /q` path was split, and source, Git and backups went. Sebastian Spicker (5 Oct 2026): a guard was relaxed to allow any path "outside the repository root", a test passed it the parent directory, and `rm` deleted every sibling checkout.
Example
The empty-target case from 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
Copy-paste fixes
Pick the files for the agents you use. Snippets are in English so they work in any repo.
Claude Code: add to 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 coding agent and others: add to 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: project rule
.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 hook: refuse deletes with empty or dangerous targets
Register with matcher "Bash". It checks the command text only; the real protection is the OS sandbox plus backups.
.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 hook: stop destructive shell commands
Register with matcher "Bash". A pattern list is a seatbelt, not a sandbox; keep real backups and least-privilege credentials too.
.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)