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.

Some specifics below are marked "as of CLI 2.1.x" — verify against your own version; this isn't official docs.

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:

LevelFileScope
Enterprise managedan org-deployed managed-settings file, outside any repoevery user on every machine the org controls
CLI arguments--permission-mode, --allowedTools, etc.this invocation only
Local project.claude/settings.local.jsonyour checkout, meant to be gitignored
Shared project.claude/settings.jsonthe repo, committed, team-wide
User~/.claude/settings.jsonevery 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:

  1. Hooks get first look (PreToolUse, then PermissionRequest) and can return allow, deny, ask, or defer — see the hooks guide for the exact contract.
  2. Deny rules — an explicit match always wins from here down, no matter what else says otherwise.
  3. Ask rules — forces an interactive prompt even for something the mode below would otherwise wave through.
  4. The permission mode — see the next section.
  5. 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:

ModeBehavior
defaultPrompt interactively for anything not covered by a rule.
acceptEditsAuto-allow file edits (Edit, Write); still prompt for everything else, including Bash and MCP tools.
planResearch only — mutating tools are refused outright rather than prompted for.
bypassPermissionsNever 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.