Claude Code's permission rules let you write allow / ask / deny entries in settings.json to specify, fine-grained, which tools, commands, files, and domains run without asking, prompt every time, or are forbidden. They can be shared across a team, so you block the dangerous stuff for sure while letting safe, routine work run uninterrupted.

This guide covers what permission rules are (and how they differ from permission modes), the allow/ask/deny precedence, the rule syntax, the settings.json hierarchy, and practical recipes — all based on the official spec.

CLAUDE CODE · PERMISSION RULES

Fine-grained control with allow / ask / deny

Evaluated deny → ask → allow (first match wins)

deny

Forbidden (wins over everything)

ask

Prompt every time

allow

Run without asking

1. What permission rules are (vs modes)

Claude Code uses a tiered permission system. Reads (file views, Grep, etc.) need no approval; Bash commands and file edits do — and on top of that baseline, rules let you specify fine-grained exceptions (source: Claude Code's official "Configure permissions").

It's easy to confuse rules with permission modes. Modes are the broad "how often it asks" baseline (default/acceptEdits/auto, etc.); rules are per-tool, per-command specifications. They work together — the mode sets the baseline, and rules override it with "always allow this" or "never allow that."

💡 Rules are enforced by Claude Code, not the model. Your prompt or CLAUDE.md shapes what Claude tries to do, but not what's allowed. Grant or revoke access via /permissions, rules, modes, or a PreToolUse hook.

2. allow / ask / deny and precedence

There are three rule types. The /permissions command lists and edits them (and shows which settings.json each comes from).

ALLOW

Run without asking

The specified tool/command runs without manual approval.

ASK

Prompt every time

Asks for confirmation on each use. A matching ask prompts even if a broader allow also matches.

DENY

Forbidden (top priority)

Blocks the tool. Beats any allow, and a deny at any level always wins.

Rules are evaluated deny → ask → allow. The first match decides the outcome, and rule specificity does not change the order. A broad Bash(aws *) deny beats a more specific Bash(aws s3 ls) allow. The point: a deny cannot carry allowlist exceptions.

⚠️ Two deny forms behave differently: a bare tool name like Bash removes the tool from Claude's context entirely (Claude never sees it). A scoped rule like Bash(rm *) keeps the tool available and blocks only matching calls.

3. Rule syntax (Tool(specifier))

The format is Tool (everything) or Tool(specifier) (specific). Each tool has its own specifier style.

ToolExampleMeaning
BashBash(npm run *)Commands starting with npm run (trailing * = word boundary)
Read / EditRead(./.env)Read the .env in the current dir (gitignore-style path)
WebFetchWebFetch(domain:example.com)Fetches to example.com
MCPmcp__github__get_*The github server's get_ tools
AgentAgent(Explore)The Explore subagent

Bash wildcards can appear anywhere. Bash(ls *) (space + *) matches ls -la but not lsof (word boundary); Bash(ls*) (no space) matches both. A trailing :* is equivalent to *.

// Allow safe commands, deny git push
{
  "permissions": {
    "allow": [
      "Bash(npm run *)",
      "Bash(git commit *)"
    ],
    "deny": [
      "Bash(git push *)"
    ]
  }
}

Mind compound commands. Claude Code understands separators like && || ; |, and each subcommand must match independently. Bash(safe-cmd *) does not allow safe-cmd && rm -rf .. Note that read-only commands (ls/cat/grep/find/pwd, etc.) run without a prompt in every mode, and wrappers like timeout/nice/nohup are stripped before matching.

Read/Edit paths follow gitignore semantics with four anchors:

FormAnchorExample
//pathFilesystem absoluteRead(//tmp/x)
~/pathHomeRead(~/.ssh/**)
/pathProject rootEdit(/src/**)
path / ./pathCurrent dirRead(.env)

/Users/... is NOT absolute — it's relative to the project root. Use //Users/... (double slash) for absolute paths. Read(.env) equals Read(**/.env) and matches every .env below.

4. settings.json hierarchy and precedence

Rules live in settings.json. There are several files, and higher levels are stronger. The key rule: a deny at any level always beats an allow at any other level.

PriorityLocationUse
1 (strongest)Managed settingsOrg policy. Cannot be overridden by users/CLI
2Command-line argumentsTemporary session overrides
3.claude/settings.local.jsonPersonal project settings (gitignored)
4.claude/settings.jsonShared project settings (committed)
5~/.claude/settings.jsonUser settings across all projects
// .claude/settings.json (team-shared)
{
  "permissions": {
    "defaultMode": "acceptEdits",
    "allow": ["Bash(npm run *)", "WebFetch(domain:docs.example.com)"],
    "deny": ["Read(.env)", "Read(**/secrets/**)", "Bash(git push *)"],
    "additionalDirectories": ["../shared-lib"]
  }
}

The default permission mode is set here too, as defaultMode (mode details). To read/edit outside the working directory, add paths to additionalDirectories.

5. Practical recipes

Common combinations. The basic shape: deny the dangerous stuff for sure, allow the safe routine to automate it.

🔒 Protect secret files

deny: ["Read(.env)", "Read(**/secrets/**)", "Read(~/.ssh/**)"]. Block the reads outright.

🚀 Always confirm risky ops

ask: ["Bash(git push *)", "Bash(rm *)"]. Forces a prompt even in auto mode.

⚡ Automate routine work

allow: ["Bash(npm run *)", "Bash(git commit *)"]. Tests/builds/commits run uninterrupted.

Restricting URLs via Bash argument patterns is fragile (curl http://github.com/ * is easily bypassed by -X GET or variable expansion). Instead, deny curl/wget and allow specific domains with WebFetch(domain:...). For stricter control, validate URLs in a PreToolUse hook.

6. Gotchas and common mix-ups

  • Deny is enforced by Claude Code, not the model. Writing "don't read this" in your prompt or CLAUDE.md won't stop it without a rule.
  • Read/Edit deny can't stop indirect access. It applies to built-in file tools and cat/head/sed, but not to a Python or Node script that opens files itself. For OS-level enforcement, also use sandboxing.
  • Watch environment runners. devbox run *, npx, and docker exec run their arguments as a command, so Bash(devbox run *) also permits devbox run rm -rf .. Write rules that include the inner command.
  • Hooks don't override rules. A PreToolUse hook can extend permissions, but deny/ask are evaluated regardless of what the hook returns (deny-first is unchanged).

※ Behavior is per Claude Code's official docs (Configure permissions / Settings), as of June 2026. It can change — check the official docs for the latest.

Summary

Three takeaways on Claude Code permission rules.

  • What they are: allow/ask/deny in settings.json to grant/prompt/forbid per tool, command, file, and domain. Modes set the baseline; rules handle the specifics.
  • Precedence: deny → ask → allow (first match wins; specificity is irrelevant). A deny at any level always wins. Levels: managed > CLI > local > project > user.
  • Syntax: Tool(specifier). Bash uses wildcards (space + * is a word boundary), Read/Edit use gitignore-style paths, WebFetch uses domain:. Compound commands need every subcommand to match.

"Block the dangerous stuff for sure, automate the safe routine" is the heart of rule design. Combine it with permission modes, hooks, and the effort setting to run Claude Code safely and smoothly.

FAQ

Q. How are permission rules different from modes?

A. Modes are the broad confirmation baseline (default/acceptEdits/auto, etc.); rules are per-tool, per-command specifications. They work together, and rules override modes (e.g. ask/deny rules still apply in auto mode).

Q. I allowed it — why does it still prompt?

A. Because evaluation is deny → ask → allow. If a separate ask (or deny) also matches the call, it wins even over a more specific allow. Specificity does not change the order.

Q. Where do I put settings.json?

A. Team-shared: .claude/settings.json (committed). Personal: .claude/settings.local.json (gitignored). Across all projects: ~/.claude/settings.json. For org-wide enforcement, use managed settings (cannot be overridden).

Q. How do I make sure .env is never read?

A. Set deny: ["Read(.env)", "Read(**/secrets/**)"]. Note this applies to built-in file tools and cat-style commands, but not to indirect reads via a script. For OS-level enforcement, also enable sandboxing.

Q. Can I restrict URLs or files via Bash arguments?

A. Not reliably. curl http://github.com/ * is easily bypassed by options or variable expansion. For URL limits, deny curl/wget and use WebFetch(domain:...), or validate in a PreToolUse hook.