Bug

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.

Rules

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.
Rules

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 rule

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.
Hook

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)
Hook

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)

Sources and credit

Related patterns

翻车 of the week

One real agent failure and its fix, every week. Free, short, no spam.