Skip to main content
The permissions system controls whether Claude Code runs a tool immediately, prompts you for confirmation, or blocks the call entirely. You configure it through permission modes and rule lists that can be set globally, per project, or per session.

Permission modes

A permission mode sets the baseline behavior for all tool calls. Individual allow/deny rules can override this baseline for specific tools or commands.
ModeDescription
defaultPrompt for approval on potentially dangerous operations. Safe read-only tools run without prompting.
acceptEditsAuto-approve all file edit operations (Write, Edit, MultiEdit). Bash commands still prompt.
bypassPermissionsSkip all permission checks. All tools run without prompting. Requires allowDangerouslySkipPermissions: true in settings.
planRead-only planning mode. No tool execution; Claude can only read files and explain what it would do.
dontAskDo not prompt the user. Deny any tool call that is not pre-approved by an explicit allow rule.
bypassPermissions mode disables all safeguards. Only use it in sandboxed environments or CI pipelines where you fully control the input. The mode requires allowDangerouslySkipPermissions: true in your settings file.

Setting the permission mode

Pass --permission-mode to set the mode for a single invocation:
claude --permission-mode acceptEdits
claude --permission-mode bypassPermissions --dangerously-skip-permissions
claude --permission-mode plan

Permission rules

Rules let you pre-approve or block specific tools and commands without changing the global permission mode. Rules are additive across settings scopes — allow rules from all files merge together.

Configuration format

{
  "permissions": {
    "allow": [
      "Bash(git *)",
      "Bash(npm run *)",
      "Read",
      "Write(src/**)",
      "mcp__myserver"
    ],
    "deny": [
      "Bash(rm -rf *)",
      "Bash(curl * | bash)",
      "Write(/etc/**)"
    ]
  }
}
allow
string[]
Rules that auto-approve matching tool calls without prompting.
deny
string[]
Rules that unconditionally block matching tool calls.
Deny rules always take precedence over allow rules. If both an allow rule and a deny rule match a tool call, the call is blocked.

Rule syntax

A rule is a string that matches a tool name, optionally with a parenthesized content pattern. Tool name only — matches every call to that tool:
Read
Write
Edit
Bash
Tool name with content pattern — matches calls where the tool’s primary input matches the glob:
Bash(git *)
Bash(npm run *)
Write(src/*)
Edit(*.ts)
For Bash, the pattern is matched against the full command string. For file tools (Write, Edit, Read, Glob), the pattern is matched against the file path argument. MCP server — matches all tools from a specific MCP server:
mcp__myserver
MCP server wildcard — same as above, explicit wildcard form:
mcp__myserver__*
Specific MCP tool — matches one tool from a server:
mcp__myserver__query_database
Agent type — blocks or allows a specific subagent:
Agent(Explore)
Agent(CodeReviewer)

Pattern matching for Bash

Bash rule patterns use shell-style glob matching on the full command string.
{
  "permissions": {
    "allow": [
      "Bash(git status)",
      "Bash(git log *)",
      "Bash(git diff *)",
      "Bash(npm run test*)",
      "Bash(make *)"
    ],
    "deny": [
      "Bash(rm *)",
      "Bash(sudo *)",
      "Bash(* | bash)",
      "Bash(* | sh)"
    ]
  }
}
Bash patterns match prefix-first. A rule like Bash(git *) matches git status, git log --oneline, and git push --force. Be specific when writing deny rules.

Pattern matching for file tools

File path patterns are matched against the absolute path of the file being read, written, or edited.
{
  "permissions": {
    "allow": [
      "Write(src/**)",
      "Write(tests/**)",
      "Edit(*.md)",
      "Read"
    ],
    "deny": [
      "Write(/etc/**)",
      "Write(~/.ssh/**)",
      "Edit(.env*)"
    ]
  }
}

Permission rule sources and priority

Rules are collected from multiple sources and evaluated in a defined order. When the same tool call matches rules from different sources, deny takes precedence over allow, and the most restrictive result wins.
SourceWhere configuredEditable
policySettingsManaged policy layerNo
flagSettingsCLI flags and SDK control requestsPer-session
userSettings~/.claude/settings.jsonYes
projectSettings.claude/settings.jsonYes
localSettings.claude/settings.local.jsonYes
cliArg--allowedTools / --disallowedTools flagsPer-invocation
session/permissions command, SDK updatesPer-session

--allowedTools and --disallowedTools CLI flags

Pass comma-separated rule strings directly on the command line:
claude --allowedTools "Bash(git *),Read,Write" --print "Run the tests"
claude --disallowedTools "Bash,Write" --print "Summarize this project"
These map to the cliArg source and apply for the duration of the invocation.

Permission decisions

The permission engine evaluates each tool call through a pipeline and returns one of three outcomes.
DecisionMeaning
allowTool runs immediately.
askUser is prompted for confirmation.
denyTool call is blocked; Claude receives an error result.

Decision pipeline

Claude Code evaluates tool calls in this order:
  1. Deny rules — if any deny rule matches, the call is blocked immediately.
  2. Ask rules — if any ask rule matches, the permission dialog is shown.
  3. Tool’s own permission check — the tool’s checkPermissions method runs (e.g., Bash checks individual subcommands).
  4. Safety checks — paths inside .git/, .claude/, .vscode/, and shell config files always prompt, even in bypassPermissions mode.
  5. Mode checkbypassPermissions and plan mode apply here.
  6. Allow rules — if an allow rule matches, the call is approved.
  7. Default behavior — if no rule matched, prompt the user.
Safety checks (step 4) are bypass-immune. Even with bypassPermissions mode, Claude Code will prompt before modifying files in .git/ or shell configuration files like ~/.bashrc.

Working directories

By default, Claude Code restricts file operations to the current working directory and its subdirectories. You can grant access to additional directories.

Via CLI flag

claude --add-dir /path/to/extra/dir

Via settings

{
  "permissions": {
    "additionalDirectories": [
      "/shared/data",
      "/home/user/configs"
    ]
  }
}

Via SDK control request

{
  "type": "control_request",
  "request_id": "dirs-1",
  "request": {
    "subtype": "apply_flag_settings",
    "settings": {
      "permissions": {
        "additionalDirectories": ["/shared/data"]
      }
    }
  }
}

Permission updates via the SDK

SDK hosts (IDEs, desktop apps) can respond to can_use_tool control requests and include permission updates to persist rule changes alongside their decisions.

PermissionUpdate object

{
  "type": "addRules",
  "rules": [
    { "toolName": "Bash", "ruleContent": "git *" }
  ],
  "behavior": "allow",
  "destination": "userSettings"
}
type
'addRules' | 'replaceRules' | 'removeRules' | 'setMode' | 'addDirectories' | 'removeDirectories'
required
The update operation.
rules
PermissionRuleValue[]
Rules to add, replace, or remove. Each rule has toolName and an optional ruleContent (the parenthesized pattern).
behavior
'allow' | 'deny' | 'ask'
required
Which rule list to modify.
destination
'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg'
required
Where to persist the update. session applies only to the current session; userSettings, projectSettings, and localSettings write to the corresponding settings files on disk.

Permission decision response

When responding to a can_use_tool request, include updatedPermissions to persist rule changes:
{
  "type": "control_response",
  "response": {
    "subtype": "success",
    "request_id": "<request_id>",
    "response": {
      "behavior": "allow",
      "updatedPermissions": [
        {
          "type": "addRules",
          "rules": [{ "toolName": "Bash", "ruleContent": "git *" }],
          "behavior": "allow",
          "destination": "userSettings"
        }
      ],
      "decisionClassification": "user_permanent"
    }
  }
}
behavior
'allow' | 'deny'
required
The permission decision.
updatedInput
Record<string, unknown>
Modified tool input to use instead of the original. Only valid when behavior is "allow".
updatedPermissions
PermissionUpdate[]
Permission rule updates to apply and persist alongside this decision.
decisionClassification
'user_temporary' | 'user_permanent' | 'user_reject'
How the user responded, for telemetry. user_temporary for allow-once; user_permanent for always-allow; user_reject for deny.

Hooks and permissions

PreToolUse and PermissionRequest hooks can inject permission decisions programmatically. See Hooks reference for details on permissionDecision output and hookSpecificOutput.decision. The hook-based permission flow is especially useful for headless agents that cannot show interactive prompts. When shouldAvoidPermissionPrompts is true (background agent mode), Claude Code runs PermissionRequest hooks before falling back to auto-deny.

Safety recommendations

Use bypassPermissions only in isolated, short-lived environments where you control all inputs. Set explicit deny rules for destructive operations as a defense-in-depth measure:
{
  "defaultPermissionMode": "bypassPermissions",
  "allowDangerouslySkipPermissions": true,
  "permissions": {
    "deny": [
      "Bash(rm -rf *)",
      "Bash(sudo *)",
      "Bash(curl * | bash)"
    ]
  }
}
Use default mode with allow rules for common safe operations. This minimizes interruptions while keeping you in control of destructive actions:
{
  "defaultPermissionMode": "default",
  "permissions": {
    "allow": [
      "Read",
      "Glob",
      "Grep",
      "Bash(git status)",
      "Bash(git log *)",
      "Bash(git diff *)",
      "Bash(npm run *)",
      "Bash(make *)"
    ]
  }
}
Use plan mode when you want Claude to reason about code without making any changes. Claude can read files and explain what it would do, but cannot write, edit, or run commands:
claude --permission-mode plan "Explain the architecture of this codebase"
Use dontAsk mode combined with an SDK PermissionRequest hook to replace the interactive dialog with your own approval UI. The hook receives every tool call that would have prompted and can allow, deny, or forward it to a human reviewer:
{
  "defaultPermissionMode": "dontAsk",
  "permissions": {
    "allow": [
      "Read",
      "Bash(git *)"
    ]
  }
}
Any tool call not matching an allow rule is sent to your PermissionRequest hook handler instead of being auto-denied.