Claude Code’s permission system is one of its most practical features, and also one of its most misunderstood. Many developers default to accepting prompts one by one, never configuring the rules layer. That works for a solo developer on a personal project. For a team, for CI/CD, or for any environment where you care about what Claude can do without asking, the permission system is worth understanding in full.
This guide covers all six permission modes, the complete rule syntax, and the managed settings layer for enterprise deployments.
The Permission System in 60 Seconds
Claude Code runs a tiered permission check before every tool use:
| Tool type | Default behavior | Sticky approval |
|---|---|---|
| Read-only (file reads, Grep, Glob) | No prompt | N/A |
| Bash commands | Prompt on first use | Permanent per project + command |
| File modification (Edit, Write) | Prompt on first use | Until session end |
The evaluation order is always: deny → ask → allow. The first matching rule wins. A deny rule at any scope beats an allow rule at any scope.
The Six Permission Modes
Set the active mode in your settings file:
{
"defaultMode": "acceptEdits"
}
Or pass it as a CLI flag for a one-off session:
claude --permission-mode acceptEdits
1. default — Standard Interactive
The baseline. Claude prompts for permission on first use of each tool in each project. Approvals for Bash commands are permanent per project directory and command. File edit approvals last until session end.
Best for: interactive development sessions where you want oversight on new tool uses.
2. plan — Read-Only Exploration
Claude Code’s Plan Mode. Claude can read files and run read-only shell commands to explore your codebase, understand the task, and build a plan. It cannot edit source files.
claude --plan
What’s allowed in plan mode:
- Reading any file
- Running read-only Bash commands (ls, cat, git log, grep, etc.)
- Browsing the web (if WebFetch is permitted)
- Creating and reviewing plans
What’s blocked:
- Writing or editing files
- Running Bash commands with side effects
- Executing any command that modifies state
Best for: code review, architectural analysis, getting a detailed plan before execution, situations where you want Claude’s analysis before committing to changes.
3. acceptEdits — Auto-Approve File Changes
Auto-approves file edits and a common set of filesystem commands (mkdir, touch, mv, cp, etc.) for paths within the working directory and any additionalDirectories. Bash commands that aren’t in the pre-approved list still prompt.
{
"defaultMode": "acceptEdits"
}
Best for: active development sessions where you’re working on files you own. You get Claude’s full editing capability without per-file prompts while still seeing prompts for Bash commands that could have broader effects.
4. auto — Background Safety Checks
Auto-approves tool calls using background safety checks that verify each action aligns with your original request. Still a research preview as of mid-2026.
The safety classifier examines whether a proposed tool use is coherent with what you asked for. A request to “add error handling to the auth module” that triggers an attempt to push to production would be caught and rejected.
{
"defaultMode": "auto"
}
Administrators can disable auto mode organization-wide:
{
"permissions": {
"disableAutoMode": "disable"
}
}
Best for: developers who want maximum velocity with a safety net. Understand that the safety classifier is not infallible.
5. dontAsk — Explicit Allowlist Only
The inverse of auto. Everything is denied unless explicitly pre-approved via /permissions or permissions.allow rules. Claude gets no tools it hasn’t been granted.
{
"defaultMode": "dontAsk"
}
Best for: creating tightly scoped agents, embedding Claude Code in applications where you want precise tool control, or security-sensitive environments where you need to know exactly what Claude can touch.
6. bypassPermissions — No Prompts
Skips all permission prompts. Hard circuit breakers remain: rm -rf / and rm -rf ~ still prompt.
claude --bypass-permissions
This is the mode for CI/CD pipelines and automation scripts where interactive prompts would hang execution. Only use it in isolated, disposable environments — Docker containers with no persistent state, ephemeral VMs, GitHub Actions runners that are discarded after each run.
Never use bypassPermissions on a developer machine with real files and credentials.
Administrators can lock this out organization-wide:
{
"permissions": {
"disableBypassPermissionsMode": "disable"
}
}
Permission Rule Syntax
Rules live in the permissions block of any settings file:
{
"permissions": {
"allow": ["Bash(npm run *)", "Bash(git status)"],
"ask": ["Edit(src/billing/**)"],
"deny": ["Bash(git push *)"]
}
}
Tool Matching
| Pattern | Effect |
|---|---|
Bash | Matches all Bash commands (bare name = match all) |
Bash(*) | Equivalent to Bash |
Bash(npm run build) | Matches the exact command npm run build |
Edit | Matches all file edits |
Read | Matches all file reads |
WebFetch | Matches all web requests |
Bash Wildcards
* matches any sequence of characters including spaces:
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(git commit *)",
"Bash(git * main)",
"Bash(* --version)",
"Bash(* --help *)"
],
"deny": [
"Bash(git push *)",
"Bash(npm publish *)"
]
}
}
Word boundary behavior: Bash(ls *) matches ls -la but not lsof. The space before * enforces that the prefix must be followed by a space or end-of-string. Without the space — Bash(ls*) — it matches both.
Compound commands: Bash(safe-cmd *) does NOT permit safe-cmd && other-cmd. Claude Code recognizes shell operators (&&, ||, ;, |) and checks each subcommand independently.
Process wrappers: Bash(npm test *) also matches timeout 30 npm test. Recognized wrappers that get stripped before matching: timeout, time, nice, nohup, stdbuf, and bare xargs.
The :* suffix: Bash(npm:*) is equivalent to Bash(npm *). Both match anything starting with npm .
Read and Edit Rules
File access rules use gitignore-style patterns with four anchoring modes:
| Prefix | Meaning | Example |
|---|---|---|
// | Absolute from filesystem root | Read(//etc/hosts) |
~/ | From home directory | Read(~/.zshrc) |
/ | From project root | Edit(/src/**) |
bare or ./ | From current directory | Read(*.env) |
{
"permissions": {
"allow": [
"Edit(/src/**)",
"Edit(/tests/**)"
],
"deny": [
"Edit(/src/billing/**)",
"Read(//**/.env)",
"Read(~/.ssh/**)"
]
}
}
Pattern matching follows gitignore rules: * matches within a directory, ** matches recursively across directories.
Symlink handling: Allow rules apply when both the symlink and its target match. Deny rules apply when either matches. A symlink inside an allowed directory that points outside it still prompts.
WebFetch Rules
{
"permissions": {
"allow": ["WebFetch(domain:github.com)", "WebFetch(domain:npmjs.com)"],
"deny": ["WebFetch"]
}
}
This allows fetching from GitHub and npm, blocks all other WebFetch calls.
MCP Tool Rules
{
"permissions": {
"allow": ["mcp__github__*"],
"deny": ["mcp__github__push_commit"]
}
}
Format: mcp__<server-name>__<tool-name>. Wildcards work.
Agent (Subagent) Rules
{
"permissions": {
"deny": ["Agent(Explore)", "Agent(Plan)"]
}
}
Agent(Explore) and Agent(Plan) disable those built-in subagents. Agent(my-agent) controls a custom agent.
Viewing and Managing Rules at Runtime
/permissions
This opens the permissions UI inside Claude Code, listing all active rules and the settings file they came from. Use it to:
- See every rule at all scopes merged together
- Understand why a specific tool is or isn’t being prompted
- Add quick approvals without editing settings.json directly
Settings File Precedence
Rules compose across multiple settings files. The evaluation order from highest to lowest precedence:
- Managed policy settings — Cannot be overridden by anything below
- CLI arguments (
--allowedTools,--disallowedTools) — Session-level only - Local project settings (
.claude/settings.local.json) — Per-developer, gitignored - Shared project settings (
.claude/settings.json) — Committed, team-shared - User settings (
~/.claude/settings.json) — Personal, all projects
Critical: A deny at any scope beats an allow at any scope. A user-level deny blocks a project-level allow. A managed policy deny blocks everything.
Settings File Locations
| Scope | File | Shared |
|---|---|---|
| User | ~/.claude/settings.json | No |
| Project (shared) | .claude/settings.json | Yes, commit it |
| Project (local) | .claude/settings.local.json | No, gitignore it |
| Managed policy | See managed settings section | Deployed via MDM |
Enterprise: Managed Settings
Organizations that need centralized control over Claude Code deploy managed settings that developers cannot override.
Delivery Mechanisms
macOS (MDM): Write to the key com.anthropic.claudecode in the machine-level plist.
File-based: Create the managed-settings.json file at:
- macOS:
/Library/Application Support/ClaudeCode/managed-settings.json - Linux:
/etc/claude-code/managed-settings.json - Windows:
C:\Program Files\ClaudeCode\managed-settings.json
Managed-Only Settings
These settings are only honored when placed in managed settings. Placing them in user or project settings has no effect:
| Setting | Effect |
|---|---|
allowManagedPermissionRulesOnly | When true, blocks user/project allow/ask/deny rules. Only managed rules apply |
allowManagedMcpServersOnly | Only MCP servers in managed settings load |
allowManagedHooksOnly | Only managed hooks run; user/project hooks are blocked |
permissions.disableBypassPermissionsMode | Set "disable" to block bypassPermissions mode |
permissions.disableAutoMode | Set "disable" to block auto mode |
Typical Enterprise Configuration
{
"permissions": {
"disableBypassPermissionsMode": "disable",
"disableAutoMode": "disable",
"deny": [
"Bash(git push --force *)",
"Bash(git push --force-with-lease *)",
"Bash(curl * | bash)",
"Bash(wget * | sh)",
"Bash(rm -rf /*)",
"Read(//**/.env)",
"Read(~/.aws/**)",
"Read(~/.ssh/**)"
]
},
"allowManagedPermissionRulesOnly": true,
"allowManagedHooksOnly": true,
"allowManagedMcpServersOnly": true
}
This configuration:
- Forces all permission rules to come only from managed settings
- Blocks dangerous Bash patterns org-wide
- Prevents developers from overriding with permissive local rules
- Blocks credential file access
Extending Permissions with Hooks
Static rules handle known patterns. For dynamic permission logic, PreToolUse hooks run before permission evaluation:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/validate-bash.sh"
}
]
}
]
}
}
#!/usr/bin/env bash
# .claude/hooks/validate-bash.sh
# Blocks git push to protected branches
COMMAND=$(cat /dev/stdin | jq -r '.tool_input.command // ""')
if echo "$COMMAND" | grep -qE '^git push.*\b(main|master|production)\b'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Direct push to protected branch blocked. Create a PR instead."}}'
exit 2
fi
exit 0
Exit code 2 from a PreToolUse hook blocks the tool call before any permission rule evaluation. This applies even if an allow rule would permit the command.
Important interaction: Hooks cannot bypass deny rules. If a deny rule matches, it blocks regardless of what the hook returns. Hooks add logic on top of rules — they don’t replace them.
Use hooks when:
- You need to inspect runtime state (current branch, file contents, environment variables)
- The permission decision depends on context you can’t express with static patterns
- You want to provide a specific error message explaining why something was blocked
Working Directories and Additional Directories
By default, Claude has read/write access to the directory where it was launched. Extend this with:
# Session-level
claude --add-dir ../shared-config --add-dir /mnt/data
# Persistent in settings
{
"permissions": {
"additionalDirectories": ["../shared-lib"]
}
}
Files in additional directories follow the same permission mode as the working directory. Note: additionalDirectories in settings grants file access only — it doesn’t load .claude/ configuration from those directories. The --add-dir CLI flag does load configuration (skills, settings with limited keys).
How Sandboxing and Permissions Interact
Permissions and sandboxing (sandbox.enabled: true in settings) are complementary layers:
- Permissions: Claude-level controls. Which tools Claude can use, which files Claude can access. Applied before Claude even attempts a tool call.
- Sandboxing: OS-level controls on the Bash tool specifically. Even if Claude is permitted to run a Bash command, the sandbox limits what that command can do at the OS level.
With autoAllowBashIfSandboxed: true (the default when sandboxing is enabled), sandboxed Bash runs without prompting. The sandbox boundary replaces per-command prompts. Explicit deny rules still apply.
Use both for defense-in-depth: permissions prevent Claude from attempting restricted actions, sandboxing prevents Bash subprocesses from reaching restricted resources even if prompt injection bypasses Claude’s decision-making.
Practical Configurations
Solo Developer: Productive and Safe
{
"defaultMode": "acceptEdits",
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(yarn *)",
"Bash(git status)",
"Bash(git log *)",
"Bash(git diff *)",
"Bash(git add *)",
"Bash(git commit *)",
"Bash(git checkout *)",
"Bash(git branch *)"
],
"deny": [
"Bash(git push --force *)",
"Bash(git push --force-with-lease *)",
"Bash(npm publish *)",
"Read(~/.ssh/**)",
"Read(//**/.env)"
]
}
}
CI/CD Runner (Isolated Container)
{
"defaultMode": "bypassPermissions"
}
Only appropriate because the CI runner is ephemeral and isolated. Nothing persists after the run.
Code Review Agent (Read-Only)
{
"defaultMode": "plan",
"permissions": {
"allow": [
"Bash(git log *)",
"Bash(git diff *)",
"Bash(git show *)",
"WebFetch(domain:github.com)"
]
}
}
Claude can read everything and analyze freely, but cannot modify any file.
Scoped Feature Agent
{
"defaultMode": "dontAsk",
"permissions": {
"allow": [
"Read(src/auth/**)",
"Edit(src/auth/**)",
"Bash(npm run test:auth)",
"Bash(npm run lint src/auth/**)"
]
}
}
Claude can only touch src/auth/, run auth tests, and lint auth files. Everything else is denied by dontAsk mode.
The Read-Only Command Exemption
Claude Code maintains a built-in list of Bash commands treated as read-only. These run without any permission prompt in every mode:
ls, cat, echo, pwd, head, tail, grep, find, wc, which, diff, stat, du, cd, and read-only git commands.
You cannot add to this list. You can override it by adding explicit ask or deny rules:
{
"permissions": {
"deny": ["Bash(cat *)"]
}
}
This would block cat even though it’s normally read-only.
Internal Links
- Claude Code Hooks Complete Reference — Full hook system including PreToolUse for dynamic permission enforcement
- Claude Code Memory System — Where CLAUDE.md fits in relation to permission settings
- Claude Code MCP Setup Guide — MCP permission rules for
mcp__server__toolpatterns - Claude Code Best Practices — General workflow configuration
- Claude Code Security Rules OWASP Guide — Security-focused hook and permission configurations
- Claude Code VS Code Extension: Complete Guide 2026 — permission mode switching in the VS Code panel UI