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.
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.
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: 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.
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)
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
- @rfdslabs on X: Claude Code denied rm, then used find -delete and find -exec rm -rf
- @Avani_Gupta__ on X: "The sandbox blocked one of my commands, and I worked around it."
- @marshalcodes on X: rm is just one spelling of "delete .venv"
- Ona: How Claude Code escapes its own denylist and sandbox
- Claude Code docs: Configure permissions