Claude Code permissions: allow rules, ask rules, modes, and where they're read from
The permission system is four separate mechanisms that happen to share one name: which files get read, in what order; how a rule string matches a tool call; which blanket mode you're in; and, more recently, a sandbox layer that doesn't ask permission at all so much as make the answer not matter as much. Here's each one, and where the edges between them are fuzzy enough that you should verify against your own CLI version rather than trust this page as gospel.
On this page
Where settings are read from
Permission config can live in five places, and Claude Code merges all of them rather than letting one file win outright. Highest precedence first:
| Level | File | Scope |
|---|---|---|
| Enterprise managed | an org-deployed managed-settings file, outside any repo | every user on every machine the org controls |
| CLI arguments | --permission-mode, --allowedTools, etc. | this invocation only |
| Local project | .claude/settings.local.json | your checkout, meant to be gitignored |
| Shared project | .claude/settings.json | the repo, committed, team-wide |
| User | ~/.claude/settings.json | every project on your machine |
The precedence order matters differently depending on what's being merged. The
permission mode is a single value — whichever level sets it highest in this table wins
outright, full stop. The allow/ask/deny arrays are unioned across every level
that defines one, not replaced — a deny rule your org's managed settings adds still applies even
if your project's settings.json never mentions it, and a project-level allow rule
doesn't need to be repeated in your user settings to take effect there too. As of CLI 2.1.x this
union-not-override behavior for the rule arrays is the practical model to plan around; enterprise
managed settings additionally get to lock specific keys so nothing lower in the table can
override them regardless of array-merge semantics — check your own version's behavior with a
throwaway deny rule before relying on layering across a real org deployment.
Rule syntax: Tool(pattern)
A rule is a tool name, optionally followed by a pattern in parentheses that narrows which calls to that tool it covers. Leaving the parentheses off matches every call to that tool:
{
"permissions": {
"allow": [
"Bash(git diff:*)",
"Bash(npm test:*)",
"Read(./src/**)",
"WebFetch(domain:api.example.com)"
],
"ask": [
"Bash(git push:*)"
],
"deny": [
"Bash(rm -rf:*)",
"Read(./.env)",
"Read(./.ssh/**)"
]
}
}
The pattern syntax isn't uniform across tools, which is the part worth internalizing rather than
guessing at: Bash(...) patterns are prefix matches ending in :* against
the command string, not shell globs or regex — Bash(git push:*) matches
git push origin main but not cd /tmp && git push, since the match
is against the start of the string Claude Code is about to execute. Read,
Write, and Edit patterns are filesystem globs relative to the project
root. WebFetch(domain:...) matches a hostname, not a full URL pattern. An MCP tool's
rule is its full mcp__<server>__<tool> name with no pattern at all — you
allow or deny a specific MCP tool wholesale, there's no sub-matching on that tool's own arguments
the way there is for Bash commands.
How allow, ask, and deny interact
For a single tool call, the checks run in a fixed order and the first one that produces a verdict wins:
- Hooks get first look (
PreToolUse, thenPermissionRequest) and can returnallow,deny,ask, ordefer— see the hooks guide for the exact contract. - Deny rules — an explicit match always wins from here down, no matter what else says otherwise.
- Ask rules — forces an interactive prompt even for something the mode below would otherwise wave through.
- The permission mode — see the next section.
- Allow rules — the last check before falling through to an actual prompt.
Put plainly: a narrower, more specific rule doesn't beat a broader one just for being narrower —
deny beats everything regardless of which list it came from or how specific its
pattern is. Two rules that both match the same call and disagree resolve by category
(deny > ask > allow), never by whichever one happens to be more specific.
defaultMode: default, acceptEdits, plan, bypassPermissions
permissions.defaultMode in settings.json (or --permission-mode
on the CLI for one run) sets the fallback behavior for anything the rule lists above don't resolve:
| Mode | Behavior |
|---|---|
default | Prompt interactively for anything not covered by a rule. |
acceptEdits | Auto-allow file edits (Edit, Write); still prompt for everything else, including Bash and MCP tools. |
plan | Research only — mutating tools are refused outright rather than prompted for. |
bypassPermissions | Never prompt. Equivalent to --dangerously-skip-permissions; see that guide for exactly what this turns off. |
{ "permissions": { "defaultMode": "acceptEdits" } }
bypassPermissions doesn't touch hooks or deny rules — those still run first — it
just removes the "nothing else matched, so ask" step underneath them. In practice that means most
teams reaching for it haven't bothered writing deny rules either, since avoiding prompts entirely
was the whole point.
Sandbox interplay
As of CLI 2.1.x, Claude Code also ships an OS-level sandbox for the Bash tool
(seatbelt on macOS, a namespace/bubblewrap-style restriction on Linux) that is a genuinely separate
layer from everything above: the permission system decides whether to ask; the sandbox
decides what a command can actually reach once it's running, independent of who approved
it. A command that's confined to a scratch filesystem view with no network egress is a smaller
risk if wrongly auto-allowed than the identical command running unsandboxed with your real
credentials and a live network — which is why a sandboxed session can reasonably carry more
permissive allow rules or a broader acceptEdits/bypassPermissions mode
than the same rules would justify outside one. The two layers don't know about each other's
config: turning on the sandbox doesn't change what settings.json's rules say, and a permissive
rule set doesn't turn the sandbox on. Treat them as independently-configured, mutually
reinforcing controls, and verify the sandbox is actually active (rather than silently unavailable
on your platform or container) before treating it as the reason a broader permission mode is
safe.
Checking what's actually in effect
Five files that merge by unclear-until-tested rules is exactly the setup that produces "why did
it just allow that" surprises. Before trusting a layered config in production, force a test case
in each direction — a command you expect denied, one you expect auto-allowed — and confirm the
actual behavior matches what you think the merged rule set says, rather than reasoning about the
precedence table in the abstract. This matters most right after any change to the
highest-precedence file in play (an enterprise policy push, a CI runner's --permission-mode
flag) since that's exactly the layer most likely to silently override an assumption baked into a
lower one.