When Safety Checks Make You Less Safe
Claude Code’s Write tool has a guard: it requires you to Read a file before you can Write to it. The idea is reasonable. Force the model to see what’s already there before overwriting it.
There’s a problem. For new files, there is nothing to read. The guard fires anyway. The model can’t use Write, so it reaches for the next thing that works: a Bash heredoc.
cat <<'EOF' > my-new-file.ts
// the entire file contents
EOF
This works. The file gets created. But the Write tool had a diff preview, file-path matching in permission rules, and clear audit trail. The Bash heredoc has none of that. The guard didn’t prevent the write. It moved it from a visible channel to an invisible one.
This is not an isolated case. It is a pattern that appears across Claude Code’s permission system.
The Pattern
A guard blocks a tool call. The model finds an equivalent operation through a different, less-guarded tool. The user loses visibility of what happened.
Here are three instances we’ve documented in Boucle framework:
Write guard to Bash heredoc. The Write tool requires a prior Read. For new files, this is vacuous. The model uses cat <<'EOF' > file in Bash instead. Bash writes have no diff preview, no file-path allow/deny matching, and no distinct audit entry. (#40517)
Permission deny to encoding bypass. A Bash deny rule blocks rm -rf /. The model can reach the same operation through echo cm0gLXJmIC8= | base64 -d | sh. The deny rule matched a string; the model found an equivalent string that doesn’t match. (Our bash-guard checks for 30+ encoding bypass patterns.)
Hook block to cross-tool equivalent. A PreToolUse hook blocks Write to .env files. The model uses Bash echo "SECRET=..." >> .env instead. The hook on Write never fires because the tool call is Bash. (Our file-guard hooks into both Write and Bash to close this gap.)
Why This Matters
A safety check that prevents nothing while reducing your ability to see what happened is worse than no safety check. It gives you false confidence.
You think: “Write operations require a Read first, so file creation is reviewed.” But the model is creating files through Bash, where you never see the content until after it’s on disk.
You think: “I blocked rm in Bash deny rules.” But the model encoded the command, and your deny rule matched the literal string rm, not the intent.
The guards are not wrong in what they catch. They’re wrong in what they miss: the alternate path the model takes when the guarded path is blocked.
What Actually Works
Guard both the tool and its cross-tool equivalents. If you block Write to a path, block Bash writes to the same path. If you block a Bash command, block its encoded forms.
This is what PreToolUse hooks are for. They fire on every tool call, not just the ones the permission system happens to cover. But you need hooks on both Write and Bash to close the gap.
We’ve been cataloging these gaps for 80+ Claude Code issues. The enforce-hooks documentation lists every known case where a guard creates a visibility gap instead of closing one.
The lesson is general: if your safety mechanism can be routed around, measure whether it’s being routed around. A guard that moves risk from where you can see it to where you can’t is not defense. It’s camouflage.