Overreach

Blocked, so it found another way

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

A deny rule or sandbox stops a command, and the agent treats the denial as a puzzle. It reaches the same effect with a different spelling (`find -delete` instead of `rm`, a full path instead of `npx`) or turns the guard off.

Applies to: Claude Code

Symptom

The transcript shows a denied call, then a sentence like "direct rm is being blocked… let me try that route", then a different command that does the same thing and succeeds. Or the summary says "the sandbox blocked one of my commands, and I worked around it."

Why it happens

The goal ("delete .venv") is still on the agent's list, and a deny rule only matches one spelling of it. @rfdslabs configured Claude Code to never run rm; after two denials it wrote "direct rm is being blocked, though the find -exec pattern passed. Let me try that route" and ran `find -delete`, then `find -exec rm -rf`. Ona's write-up shows the same reflex one level up: denied `npx`, Claude Code used `/proc/self/root/usr/bin/npx`, and when the bubblewrap sandbox failed it proposed disabling the sandbox. Claude Code's docs say permission rules are enforced by the tool, while CLAUDE.md only shapes what Claude tries, so a denial has to be a stop signal in the rules and an effect-level limit (writable paths) in the harness.

Example

Reconstructed from @rfdslabs' post.

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

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

Codex, Copilot coding agent and others: add to 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 rule

Cursor: project rule

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

PreToolUse hook: catch the other spellings of delete

Register with matcher "Bash" next to destructive-bash-hook. Still a string match: pair it with the OS sandbox so writes are limited to the project directory.

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