Logo Vincent
Back to all posts

Claude Code settings.json Deep Dive (Part 2): The Permissions System

Claude
Claude Code settings.json Deep Dive (Part 2): The Permissions System

Why permissions matter

In Part 1, we covered Claude Code’s five-layer configuration system — where the files live and which one wins. This post focuses on the permissions field, which controls what Claude is allowed to do, what it’s blocked from doing, and what requires your explicit approval.

Get it right, and Claude can work freely within the boundaries you set. Get it wrong, and you’re either buried in confirmation dialogs or you’ve given Claude more access than intended.

The full permissions object

{
  "permissions": {
    "allow": [],
    "deny": [],
    "ask": [],
    "defaultMode": "default",
    "additionalDirectories": []
  }
}

Five fields, two categories:

  • Rule arrays: allow / deny / ask — each element is a permission rule string
  • Global settings: defaultMode (default behavior) and additionalDirectories (extra paths)

Rule format: two forms

Every permission rule is a string in one of these formats:

ToolName
ToolName(content)
  • ToolName: the tool name, capitalized — e.g. Bash, Write, Read
  • (content): an optional content pattern inside parentheses — a command string or path glob

Example:

"allow": [
  "Bash",
  "Bash(npm install)",
  "Bash(npm run:*)",
  "Write(src/**)",
  "mcp__github__create-pull-request"
]

"Bash" alone allows all Bash commands without restriction. "Bash(npm install)" allows only that exact command. "Bash(npm run:*)" uses a wildcard to allow any npm run variant.

Three rule behaviors

ArrayEffect
allowAlways allowed — no confirmation dialog shown
denyAlways blocked — Claude receives a rejection immediately
askAlways prompts for confirmation, regardless of defaultMode

Priority: deny > allow > ask > defaultMode fallback.

Common pattern — allow specific commands, deny everything else:

{
  "permissions": {
    "allow": ["Bash(npm:*)", "Bash(git:*)"],
    "deny": ["Bash"]
  }
}

The allow entries carve out exceptions for npm and git commands. The deny entry catches everything else. The matching logic evaluates the most specific rule first — it’s not based on array order.

Wildcard syntax

Legacy: prefix matching with prefix:*

Bash(npm:*)
Bash(git:*)
Bash(docker:*)

Format: ToolName(prefix:*). Matches prefix or prefix <anything>, with a word boundary enforced — so npm:* will not accidentally match npmx.

This is the original wildcard syntax and still works today. It’s mainly useful for matching a CLI tool’s top-level command prefix.

Modern: pattern wildcards with *

Bash(npm * --save)
Bash(git * main)
Bash(* install)
Write(src/**/*.ts)

* matches any sequence of characters, including spaces. This form is more flexible — you can match the middle of a command, the tail, or multi-level directory paths.

Special behavior: if the pattern ends with * and contains only one wildcard, that trailing wildcard is optional. So Bash(git *) matches git add, git checkout, and the bare git command.

Escape sequences

If your command or path contains literal parentheses or asterisks, escape them:

CharacterEscaped formMeaning
(\(Literal open paren
)\)Literal close paren
\\\Literal backslash
*\*Literal asterisk

Example:

"allow": ["Bash(python -c \"print\\(1\\)\")"]

This rule matches the literal command python -c "print(1)".

MCP tool permission rules

MCP tools use a double-underscore format:

mcp__serverName
mcp__serverName__toolName
  • mcp__github: allows all tools from the GitHub MCP server
  • mcp__github__create-pull-request: allows only that specific tool
  • mcp__github__*: equivalent to mcp__github — explicit wildcard for all tools

Important: MCP rules do not support parenthesized content. The form mcp__server(pattern) is invalid.

defaultMode: global fallback behavior

When an operation doesn’t match any allow, deny, or ask rule, defaultMode determines what happens:

ValueBehavior
defaultShow a confirmation dialog (the default)
acceptEditsAuto-approve file edits; still prompt for shell commands
bypassPermissionsSkip all permission checks entirely (requires specific conditions to enable)
dontAskAuto-approve all operations — no prompts shown
planPlan-only mode — Claude can read but not write or execute

Recommended combinations:

During development, acceptEdits lets Claude read and write files freely while still confirming shell commands. For CI or fully automated pipelines, combine explicit allow rules with dontAsk for hands-free operation.

additionalDirectories: expanding file access

By default, Claude Code can only access the current working directory and its subdirectories. additionalDirectories grants access to additional paths:

{
  "permissions": {
    "additionalDirectories": ["/Users/yourname/shared-libs", "/var/log/myapp"]
  }
}

Common use cases: cross-package access in a monorepo, reading shared config files outside the project root, or inspecting system logs.

As covered in Part 1, arrays follow the merge rule: additionalDirectories entries from multiple config layers are concatenated, not overwritten.

Rule sources and editability

Permission rules can come from different configuration sources (see Part 1), but not all sources are editable:

SourceEditableNotes
userSettingsYes~/.claude/settings.json
projectSettingsYes.claude/settings.json
localSettingsYes.claude/settings.local.json
policySettingsNoEnterprise admin policy — cannot be overridden
flagSettingsNoCLI flags or env vars — read-only

In enterprise environments, admins can lock down permission rules via policySettings. Individual user config cannot bypass these.

Practical configuration examples

Allow only npm and git, deny all other Bash:

{
  "permissions": {
    "allow": ["Bash(npm:*)", "Bash(git:*)"],
    "deny": ["Bash"],
    "defaultMode": "default"
  }
}

Allow free file writes inside src/, prompt for anything outside:

{
  "permissions": {
    "allow": ["Write(src/**)"],
    "defaultMode": "default"
  }
}

Fully automated CI environment:

{
  "permissions": {
    "allow": ["Bash", "Write", "Read"],
    "defaultMode": "dontAsk"
  }
}

Always prompt before sudo:

{
  "permissions": {
    "ask": ["Bash(sudo *)"],
    "allow": ["Bash(npm:*)", "Bash(git:*)"]
  }
}

Summary

permissions is the most important field in Claude Code’s settings to get right. The core logic is straightforward:

  • Use allow / deny / ask arrays to define your rules
  • Rules support exact match, prefix wildcards (prefix:*), and pattern wildcards (*)
  • MCP tools use double-underscore format — no parenthesized content
  • defaultMode handles operations that don’t match any rule
  • additionalDirectories extends file system access beyond the working directory

Next up: hooks — how to trigger your own shell scripts before and after Claude executes tools, enabling deeper automation and guardrails.

More Articles

© 2026 vincentqiao.com . All rights reserved.