Claude Code permission rules: ten bypasses closed in three releases
Half of the ten fixes land on guarantees the documentation makes, not on the limits it already disclaims. Two more are parts that failed open, and for a PreToolUse hook script that crashes or hangs, failing open is still the documented behavior.
Three Claude Code releases in three days, v2.1.287 to v2.1.289, carry ten changelog lines with the same shape: a deny rule, an ask rule, a hook or an organization's permission ceiling that should have stopped something and didn't. Each line reads like routine maintenance. Read as a set, they show where this layer breaks, and it isn't where the documentation warns you it might.
The docs are frank about what a Bash rule is. It matches the command text Claude writes, so:
a deny or ask rule covers the invocation Claude usually produces and
isn't a security boundary around the program.The same page lists /bin/rm -rf build/ and bash -c 'rm -rf build/' as forms a Bash(rm *) deny rule does not stop. None of the ten fixes is about that disclaimer. Five of them broke a sentence the docs state as a guarantee, and two are components that failed open. The other three sit at the edges: a surface the docs call best effort, a user plugin writing over text an organization manages, and a check that missed shell arithmetic. Everything below comes from Anthropic's changelog. I haven't reproduced the bugs; all ten are fixed as of 2.1.289.
Did Claude Code deny rules hold under sandbox auto-allow?
Not when a variable assignment came first. Two fixes in v2.1.289: Bash deny and ask rules missed a command behind an environment-variable prefix with an expanded value, and were skipped entirely when a bare variable assignment preceded the command. The changelog's own example:
TZ="$HOME" rm -rf buildBoth contradict the docs directly. The permissions page says a deny rule "matches past any leading assignment, so Bash(rm *) in deny still matches FOO=bar rm -rf tmp/", and the sandboxing page says that even in auto-allow mode:
Explicit deny rules are always respectedThe documented example uses a literal value, and the first bug needed one the shell expands. The second is harder to place: the changelog says "bare variable assignment" and no more. If that means the documented FOO=bar form, the docs' own example was the case that failed under auto-allow.
There's a tempting comfort here: auto-allow only applies to commands running inside the sandbox, so the sandbox still held. Under the default policy that doesn't help much. The sandbox bounds where a command can write, and by default the working directory is writable, so an ordinary build folder is fair game. In that setup the deny rule was the only thing that named the command.
A third fix, in v2.1.288, sits next to these two. The permission check now prompts before a BASHPID assignment whose value the shell would evaluate as arithmetic, where it used to allow it silently. That line is the whole public record, so I won't guess at the mechanism. My read is that it belongs with a case the docs already handle: in Manual mode, a write to special shell variables such as PATH or IFS prompts even inside an otherwise read-only command. Those are values the shell acts on in ways the command text doesn't show, and BASHPID arithmetic looks like one more.
Why did a dangerous rm run without a prompt in Claude Code bypassPermissions mode?
Because one of the few safeguards that survive bypass mode had two holes. The permission-modes page lists what no mode auto-approves, including:
rm and rmdir removals targeting a critical path, which no allow rule
or PreToolUse hook "allow" approvesv2.1.288 fixed a dangerous rm, one on / or the home directory, running without a prompt when it sat inside a bash -c or sh -c script, in bypassPermissions mode or under a shell allow rule (#96300). v2.1.287 fixed the same rm losing its always-ask safeguard when the command also redirected output to a ~ or wildcard path.
This floor matters more than most rules a user writes, because it's one of the few checks left for people who switched prompts off. I've argued that per-command prompts push operators toward the bypass flag, and anyone who took that road without writing deny rules was relying on this check. Wrapping the command in a second shell, or adding a redirect, was enough to step over it.
Did Claude Code Read deny rules cover files the IDE passed in?
Not when the file came through a symlink. v2.1.289 fixed Read deny rules not applying to files @-mentioned, changed or selected in the IDE through a symlink. This one isn't a broken promise. The docs say a deny rule applies "when either the requested path or the file it resolves to matches", but they also say Claude makes "a best-effort attempt" to apply Read rules to @-mentions and to the context a connected IDE shares. The IDE is a second way for file contents to reach the model, and the docs were honest that the rule follows it only loosely.
That is the coverage problem again: a check binds only over the paths it covers, and every new way of feeding context to the model is a new path.
Did managed deny rules hold over a Claude Code mod on compound commands?
Not for a nested part. On a machine with managed settings, deny rules hold over a user-installed mod by default, which was the one firm answer in what mods change about hook precedence. Separately, the docs say deny and ask rules apply "when any subcommand matches them, including a command nested inside a subshell, a command substitution, or a control-flow body". v2.1.289 fixed a deny or ask rule on a nested part of a compound command not holding over a mod's approval on managed machines. Put the two sentences together and the deny half of that bug is a broken guarantee. The ask half is less clear-cut, since the docs say a mod can approve past an ask rule; the changelog treats it as a bug on managed machines anyway.
Where did Claude Code's permission system fail open?
In two places that have nothing to do with shell parsing, plus a third entry that isn't a failure to block but belongs in the same list:
v2.1.288 PreToolUse and PermissionRequest hooks were skipped when matching
them failed or the tool input could not be serialized to JSON.
The call is now blocked.
v2.1.287 Organization per-tool permission ceilings were silently dropped
for an MCP tool named __proto__.
v2.1.289 A user-installed plugin could rewrite the descriptions of an
organization-managed MCP server's sign-in tools.The changelog says no more than that about the last two. __proto__ has special legacy behavior on ordinary JavaScript objects, which is enough to guess the class of bug and not enough to state it. The plugin entry isn't a check that let a call through; it's a user-level component writing over text an organization controls, which matters because a tool's description is what the model reads when it decides whether and how to call it.
The hook fix is the one to read carefully, because it covers less than its wording suggests. It closes failure in matching and serialization. What happens when your hook script itself fails is unchanged, and the hooks reference is explicit about it. Exit code 1 without valid JSON is a non-blocking error and the action proceeds. A script that can't start lands in the same place:
For most hook events, the action proceeds. When you set up a policy
hook, watch for this notice on its first run: a mistyped path in
settings.json leaves the gate silently disabled.And on PreToolUse, a command hook that times out "doesn't block the tool call". Only exit 2 blocks on its own. So if a hook is your gate, make every failure inside it end in exit 2. The obvious bash idiom, trap 'exit 2' ERR, isn't enough: without set -E the trap doesn't fire inside a function, and an unbound variable under set -u exits 1 without firing it at all. An EXIT trap catches every non-zero exit:
#!/bin/bash
set -euo pipefail
trap 'rc=$?; [ "$rc" -eq 0 ] || exit 2' EXITOn bash 5.2 I checked a missing command at top level, one inside a function and an unbound variable: all three exit 2 under this header, and a clean run still exits 0. The trap only catches failures the script doesn't handle itself. A failure inside an if, after ||, or buried in a command substitution is still yours to check and turn into exit 2, so test the hook with a missing dependency and malformed input before trusting it. Nothing in the script covers a hook that never starts or one that hangs: watch for the hook error notice on the first run and keep the hook fast.
What do ten Claude Code fixes say about deny rules as a safety layer?
Cut by mechanism rather than by what the docs promised, the ten fall into two clusters. One is where the check meets shell semantics: expansion, assignment, arithmetic, a nested shell, a redirect, a subcommand inside a compound. The other is where a component meets a path nobody checked or fails open: the IDE, a hook that couldn't be matched, an object key, a plugin writing over managed text. My bet is that the first cluster keeps producing fixes. The space it lives in is the shell's grammar, and a check written over command text has to anticipate every construct the shell will honor.
So I'd read deny and ask rules the way the docs ask you to: as steering for the commands Claude usually writes, and a useful one. A rule that must hold no matter how the command is spelled belongs somewhere text doesn't matter: the sandbox's own filesystem and network limits, or a credential the session was never handed. Those boundaries leak too, and in the cases I've written about they leaked through services handed to the agent for another job, not through how it spelled a command. Some rules only text can name, though, and rm -rf build is one of them. Those stay steering by nature, and the real protection for build is whatever it would take to get it back.