# Do Claude Code Deny Rules Actually Work?

The bug tracker says: sometimes. A catalog of the documented ways Claude Code deny rules fail or get bypassed — Bash pattern gaps, subagents, @-file attachments, recursive grep, symlinks — what still holds, and how to get a deny that actually denies.

*Last updated: August 31, 2026.*

Write a deny rule in `.claude/settings.json`, watch Claude Code respect it, and you'll reasonably conclude the file is protected. The issue tracker disagrees often enough that it's worth a careful look. This post is a catalog of the documented ways deny rules fail — filed by users, mostly still open — followed by what does hold.

The short version: deny rules are worth writing and worth keeping. They are also best-effort filtering inside the agent's own process, and the record shows at least five distinct classes of route around them. If a deny rule is the only thing between your agent and something you can't afford, the record says that's one layer short.

## The bug record

**Class 1: rules not enforced at all.** The bluntest reports. [#26334](https://github.com/anthropics/claude-code/issues/26334) is titled with the finding — "The deny rules simply don't deny anything" — and [#25621](https://github.com/anthropics/claude-code/issues/25621) reports Bash deny rules not enforced. [#27040](https://github.com/anthropics/claude-code/issues/27040) documents denied config files read and edited with no prompt and no error; [#27547](https://github.com/anthropics/claude-code/issues/27547) shows deny-matching commands falling through to an *ask* prompt instead of a block. [#8961](https://github.com/anthropics/claude-code/issues/8961) has the detail that should worry you most: the same denied file was readable in one session and correctly blocked in another. Intermittent enforcement is worse than none, because your test proves the rule works and the failure happens later.

**Class 2: Bash patterns are prefixes, not parsers.** A rule like `Bash(git push*)` matches a command string. `cd repo && git push`, a wrapper script, an alias, a compound command assembled mid-session — none of these look like the prefix. [#28008](https://github.com/anthropics/claude-code/issues/28008) documents denied file content reached through recursive grep and glob from an un-denied path. This isn't a bug so much as the nature of string-matching a shell: the docs themselves note that wrapper commands can't be reliably matched by prefix rules. Every regex-based hook guide ends with the same caveat, because it's true.

**Class 3: delegation routes around the list.** [#43142](https://github.com/anthropics/claude-code/issues/43142): a subagent bypasses git deny rules set in `settings.local.json`. The parent session has the policy; the delegated agent's calls don't consistently inherit the check. The same shape shows up outside Claude Code — opencode's hook layer has an open issue noting any agent can bypass restrictions by delegating to a subagent — which suggests this is what happens whenever policy lives in the session rather than under it.

**Class 4: alternate read paths.** [#6828](https://github.com/anthropics/claude-code/issues/6828) and [#61148](https://github.com/anthropics/claude-code/issues/61148): the `@`-file attachment syntax reads files that Read-tool deny rules block, including via `@../` traversal. CVE-2025-59829 covered symlink-mediated access. And the Knostic write-up on `.env` handling adds the sequencing detail that deny rules may only bind after a file has been accessed once.

**Class 5: the settings file is itself writable state.** CVE-2026-33068 documented a repository able to silently set `bypassPermissions`. Separately, security researchers demonstrated malicious hooks rewriting the permissions file to allow-list `curl`. The deny list lives in a file the session can touch; anything that can edit the file can edit the policy.

## What this adds up to

None of these classes is exotic, and naming them isn't a dunk on Claude Code — its permission system is still the most complete native one we've ranked, and several of the issues above will get fixed. The structural point outlives the individual bugs: **a deny list enforced inside the agent's own process, written in a file the process can reach, matched against strings the model composes, is best-effort by construction.** It filters the common case. It is not a boundary, and the failure mode when it slips is silent — the command just runs, and nothing in the transcript looks unusual.

That silence is the part we'd fix first. In one widely-read incident report ([#10077](https://github.com/anthropics/claude-code/issues/10077) — an `rm -rf` that took out a home directory), the session log didn't record the command that ran. The user's complaint wasn't only that the deny layer failed; it was that afterwards it was impossible to see exactly what was executed. A control that can fail *and* can't tell you it failed is two problems wearing one trenchcoat.

## What actually holds

<img src="/assets/img/screenshots/activity-denied-24h-dark.png" alt="The ACP Activity view filtered to Denied for the last 24 hours: 150 denied calls from our own agent sessions, each row naming the tool, the reason the rule fired, the tier it ran at, and the identity behind it — a Bash.sed that tried to edit the plugin's own config, curls the hook cannot inspect, and a Bash.rm held for approval." width="1440" height="900" loading="lazy" decoding="async" style="width:100%;height:auto;border:1px solid var(--line-2);border-radius:10px;box-shadow:0 20px 50px -24px rgba(0,0,0,0.9);margin:8px 0 6px">
<p style="font-size:13px;color:var(--fg-3);margin:0 0 22px">Our own workspace, today. Every deny names the rule that fired and the tier the agent was running at. Nothing here depends on the agent reading a settings file.</p>

Layered, in order of what it buys you:

1. **Keep the deny rules.** They're free, they catch the everyday case, and most sessions never test the bypass classes. Just calibrate what they are: a tripwire, not a wall.
2. **Sandbox for containment.** A devcontainer, Docker sandbox, or the native OS sandboxing bounds *where* damage can land regardless of what the model composes. This is the right answer for untrusted input, and it's the one the community converged on. Its limit is the mirror image of its strength: a wrong-but-in-bounds action — a destructive command against a database you legitimately have credentials for — sails through.
3. **Policy and audit on the tool-call path.** A [PreToolUse hook](/blog/best-ways-to-control-claude-code-2026) fires before every native tool call and MCP call, in every permission mode, `--dangerously-skip-permissions` included. Backed by a rules engine rather than a regex, it's the seam where a deny can be decided outside the agent's process — and, as important, where every call and every decision gets recorded by something the agent doesn't author. Then a deny that fails to deny is at least a visible event instead of a quiet one.

We build the third layer — the hook plus server-side policy and an audit trail is what [ACP's Claude Code integration](/integrations/claude-code) is — so weigh that disclosure as you read this. But the ranking above doesn't depend on the product: rules where they're cheap, a sandbox where input is untrusted, and enforcement-plus-record on the call path is just what the bug record recommends once you take it seriously.

## Frequently asked questions



## Where to read more

- [The Best Ways to Control What Claude Code Can Do, Ranked](/blog/best-ways-to-control-claude-code-2026) — the six options, honestly compared
- [Which Coding Agents Can You Actually Control, Ranked](/blog/coding-agent-control-surfaces-ranked-2026) — the same question across fourteen agents
- [Sandboxes and control planes](/blog/sandboxes-and-control-planes) — why containment and policy compose instead of compete
- [Your rules file is not a policy](/blog/your-rules-file-is-not-a-policy) — the same failure one level up, in prose rules
