Designing allow, deny and hooks is becoming the new code review

General Published:

Agents produce more than anyone can read. The docs draw a hard line between instruction and enforcement, and what sits on each side catches more than reading the diff after.

Verified on Nov 13, 2026 These tools change quickly. Please also check the latest official documentation. How articles are researched and verified
Contents
  1. The docs draw the line explicitly
  2. Permission design is the review checklist made executable
  3. There are three layers to design
  4. Objections and limits
  5. Three things to decide first
  6. Summary

On a team that uses coding agents daily, the highest-leverage thing to review shifts from the code itself to the design of allow, deny and hooks. Reading diffs afterwards does not go away. But a layer appears in front of it, and that layer stops more incidents than the reading does.

The argument rests on a line the tools' own documentation draws, not on a hunch.

KEY POINT

The claim

  • The docs separate instruction from enforcement, and CLAUDE.md sits on the instruction side
  • Permission rules and hooks are the executable form of the review checklist items a machine can decide
  • But permissions never review correctness — this is a layer added in front of review, not a replacement

The docs draw the line explicitly

The Claude Code memory page settles the question about organization-wide CLAUDE.md in one sentence:

Settings rules are enforced by the client regardless of what Claude decides to do. CLAUDE.md instructions shape Claude's behavior but are not a hard enforcement layer.

The same page names where to go when you need certainty:

Claude treats them as context, not enforced configuration. To block an action regardless of what Claude decides, use a PreToolUse hook instead.

A document holding your project's rules is something that gets read, and followed on a best-effort basis. Its content arrives as a user message after the system prompt, not as part of it. No amount of work sharpening its prose changes which side of the line it sits on.

That line maps directly onto review. Writing "please don't force push" in a review comment and writing it into permissions.deny differ in strength. The first may need writing again next week; the second is finished when you write it.

Permission design is the review checklist made executable

A review checklist mixes two kinds of item: the ones a person must read to judge — design intent, naming, whether the abstraction holds — and the ones a machine can decide, such as "did this touch production config" or "did a credential reach the logs."

The second kind can be written as a rule. And when you do, the docs guarantee different strengths.

Where it livesWhat the docs sayStrength
permissions.deny"Blocks before the classifier is consulted. Neither the classifier nor user intent can override it"Before the classifier; intent cannot reach it
permissions.ask"Always prompts for a command that matches a content-scoped rule like the recipe above. The classifier cannot auto-approve a matching action"Always reaches a human
Critical-path removals"Recognized rm and rmdir critical-path removals, which no allow rule or PreToolUse hook \"allow\" approves"Neither a rule nor a hook can approve it
PreToolUse hook"To block an action regardless of what Claude decides"Runs without going through Claude's judgement
CLAUDE.md"not a hard enforcement layer"Instruction

The top four rows are checklist items you can express as configuration instead of comments. For anything in deny, the probability of missing it in review is not reduced — it leaves the realm of probability.

For the mechanics, see Claude Code permissions in settings.json and settings.local.json and Claude Code permissions by example. For the hook side and where it stops being reliable, see Block rm -rf with a Claude Code PreToolUse hook, and know its limits.

There are three layers to design

"Designing permissions" covers three distinct things.

  1. Per-tool allow, ask and deny. In Claude Code that is permissions; in Codex it is named profiles, [permissions.<name>], with filesystem, workspace_roots and the network keys beneath them.
  2. Lifecycle hooks. PreToolUse and its siblings run without passing through the model's judgement. Only what you put here is in the "always" category.
  3. The execution environment itself — the sandbox and the reach of the working directory. Codex ships :read-only, :workspace and :danger-full-access as built-in profiles, which extends can narrow. For how approvals and sandboxing pair up, see Codex approval_policy versus sandbox_mode.

Three layers exist because one leaks. Claude Code's auto mode suspends broad rules the moment you enter it:

By default, narrow Bash and PowerShell allow rules such as Bash(npm test) stay in effect in auto mode. Claude Code resolves them before the classifier runs, unless the command carries per-command allowed domains. Claude Code suspends only the broad rules that grant arbitrary code execution, such as Bash(*) or wildcarded interpreters, together with every rule that names Monitor, because Monitor commands run through the shell.

So a blanket Bash(*) buys nothing in auto mode. Keeping the grain fine is not style advice; it is a precondition for the rule working at all. For how the modes differ, see Claude Code starts in auto mode by default.

Objections and limits

Four conditions make this claim fail.

Permissions never review correctness. This is the hard limit. deny stops writes to places that are off-limits; it says nothing about whether the logic written in an allowed place is right. Algorithm choice, boundary conditions, naming and design coherence stay with human readers. Permission design is not a substitute for review — it carves out the part review no longer has to carry.

It does not pay off where nothing is irreversible. On a local library with no deploys and no outbound calls, the design cost is pure prepayment. The argument holds where production, an external service, or credentials are within reach.

Rules cannot express intent. "Don't touch this directory, except during a migration" resists being written as a rule. Claude Code's classifier does let user intent override blocks, but within tight bounds:

hard_deny rules block unconditionally. User intent and allow exceptions don't apply.

Intent only reaches soft_deny, and even there the required specificity is stated: "General requests don't count as explicit intent. Asking Claude to 'clean up the repo' doesn't authorize force-pushing, but asking Claude to 'force-push this branch' does." Judgement that a rule cannot capture goes back to an in-the-moment prompt.

The configuration itself becomes something to review. A permissions file grows and turns into another thing that is hard to read. Review moves; it does not disappear.

Three things to decide first

Given those limits, the list that actually earns its place is short.

  1. Put irreversible operations in deny. Force push, production deploys, and the paths an rm -rf could reach. That is enough for the layer intent cannot override.
  2. Put anything that leaves the machine in ask. Pushes, writes to external APIs, reads of credentials. Not blocked — guaranteed to reach a person.
  3. Keep the rest narrow. When you want to write Bash(*), the question is not what to allow but which permission mode you are in.

Together that is the exercise of pulling out of your review checklist the items you keep writing every week. Whatever you pull out is reading time returned to design and correctness.

Summary

  • The docs separate instruction from enforcement, and CLAUDE.md is not on the enforcement side
  • permissions.deny, ask, critical paths and PreToolUse hooks are the executable form of review items
  • Three layers matter: rules, hooks and the execution environment. Overly broad allow rules are suspended in auto mode
  • Permissions never review correctness; this is a layer in front of code review, not a replacement
  • Start with deny for irreversible operations and ask for anything that leaves the machine

FAQ

Does good permission design make code review unnecessary?
No. Permissions govern what may happen, not whether the code that was written is correct. Nothing is removed from review; a layer is added in front of it.
Why isn't writing the prohibition in CLAUDE.md enough?
Because the docs say it isn't: "Settings rules are enforced by the client regardless of what Claude decides to do. CLAUDE.md instructions shape Claude's behavior but are not a hard enforcement layer." Anything that must hold belongs in settings or a hook.
Is this worth doing on a small team?
It depends on how many irreversible operations you have, not team size. Even three deny entries — force push, production deploys, and reading credentials out — earn their keep.

Questions & answers

Q. My CLAUDE.md says to edit only with Edit and Write and never with python or sed, yet subagents rewrite files with heredocs and python scripts. How do I actually stop it?

Answer by the editorsSource: GitHub anthropics/claude-code Issue #109143

CLAUDE.md cannot stop it, and the docs say why in one sentence:

Permission rules are enforced by Claude Code, not by the model. Instructions in your prompt or CLAUDE.md shape what Claude tries to do, but they don't change what Claude Code allows.

To make it hold, move the rule into permissions or a hook. Both reach subagents. On hooks: "Hooks from settings files, managed policy settings, and plugins also run inside subagents. When a subagent calls a tool, tool events such as PreToolUse and PostToolUse fire the same configured hooks as in the main conversation."

{
  "permissions": {
    "deny": [
      "Bash(python*)",
      "Bash(python3*)",
      "Bash(perl*)",
      "Bash(sed -i*)",
      "Bash(awk*)"
    ]
  }
}

Deny rules reach inside compound commands: "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 such as a for loop." They also match past a leading assignment.

The space before a trailing * changes the meaning. The docs' example: "Bash(ls *) requires a space after ls, so lsof doesn't match. Bash(ls*) has no space, so it matches lsof too." Drop the space if you want python3 caught as well.

For heredocs, what gets checked is the redirect target of cat > file <<'EOF': "Read and Edit deny rules apply to Claude's built-in file tools, to file commands Claude Code recognizes in Bash, such as cat, head, tail, sed, and tee, and to the targets of Bash redirections such as > file and < file." So an Edit deny rule protects the path.

A python script that opens files itself is out of reach

The same paragraph states the limit: "They don't apply to a command that reads files without naming them, such as grep -r pattern . run from the directory that holds the file, or to arbitrary subprocesses that read or write files indirectly, like a Python or Node script that opens files itself. For OS-level enforcement that blocks all processes from accessing a path, enable the sandbox." A python3 - <<'EOF' block that opens files in its own code is outside what rules can see; sandboxing is the documented enforcement.

Bash rules match command text, so they miss /usr/bin/python3 and bash -c 'python3 …'. The docs put it plainly: "It doesn't match the same program invoked in a different form", and such a rule "isn't a security boundary around the program".

You can also remove the tool from the subagent outright. Frontmatter tools is an allowlist and disallowedTools a denylist — but a disallowedTools entry with a specifier such as Bash(git push *) removes the whole tool, not just matching commands. To keep Bash and block specific commands, use the permissions.deny rules above, which apply to the main conversation and to subagents alike.

For an edit-only subagent, simply not granting Bash is the most reliable shape:

---
name: implementer
description: Implements features. Changes files only with Edit and Write.
tools: Read, Grep, Glob, Edit, Write
---

For where the line between instruction and enforcement sits, see Designing allow, deny and hooks is becoming the new code review.

Primary sources

This article was drafted by AI from official documentation and reviewed by the site operator before publishing. Found a mistake? Let us know via the contact page.