Chapter 24: Permission Modes
Learning Objectives
To configure Claude Code to stop prompting you at every step, but still automatically block dangerous actions.
Three Tiers of defaultMode
flowchart LR
Default["default
Prompt every time"] -->|Friendly| Annoy["✗ Annoying"]
AcceptEdits["acceptEdits
Edit/Write automatic
Bash still prompts"] -->|Balanced| Balance["✓ Balanced"]
Bypass["bypassPermissions
All automatic
Only deny + hook as fallback"] -->|Aggressive| Free["✓ Smooth
⚠️ Must be paired with deny + hook"]
style Default fill:#ffcdd2
style AcceptEdits fill:#fff9c4
style Bypass fill:#c8e6c9→ Multi-agent projects typically use bypass + strict deny—otherwise, frequent prompts during dispatch render autonomy ineffective.
allow / deny Syntax
Bash(<prefix>:*) Matches command prefix
Bash(<exact-string>) Exact match (less common)
Edit(<glob-pattern>) Matches file path glob
Write(<glob-pattern>) Same as above
Example:
{
"permissions": {
"defaultMode": "bypassPermissions",
"allow": [
"Bash(npm:*)",
"Bash(pytest:*)"
],
"deny": [
"Bash(sudo:*)",
"Bash(rm -rf /:*)",
"Edit(/etc/**)",
"Write(/Users/amanda/.ssh/**)"
]
}
}
deny Takes Precedence Over allow
flowchart TD
Tool["Claude wants to run X"] --> CheckDeny{Matches deny?}
CheckDeny -->|Yes| Block["✗ Immediately deny"]
CheckDeny -->|No| CheckAllow{Matches allow?}
CheckAllow -->|Yes| Pass["✓ Allow"]
CheckAllow -->|No| Mode{defaultMode?}
Mode -->|default| Ask["Prompt user"]
Mode -->|acceptEdits| AskOrPass["Edit/Write allowed
Bash prompts"]
Mode -->|bypass| Pass
style Block fill:#ffcdd2
style Pass fill:#c8e6c9Our doc2video Project's Complete deny List
"deny": [
"Bash(sudo:*)",
"Bash(curl * | sh:*)",
"Bash(curl * | bash:*)",
"Bash(wget * | sh:*)",
"Bash(wget * | bash:*)",
"Bash(chmod 777:*)",
"Bash(chown:*)",
"Bash(eval:*)",
"Bash(:(){:|:&};:*)",
"Edit(/etc/**)",
"Edit(/usr/**)",
"Edit(/System/**)",
"Edit(/Users/amanda/.ssh/**)",
"Edit(/Users/amanda/.aws/**)",
"Edit(/Users/amanda/.config/gh/**)",
"Edit(/Users/amanda/Library/**)",
"Write(/etc/**)",
...
]
Hard Limitations of Bash Pattern Matching
Bash(rm:*) blocks all commands containing rm—you cannot solely rely on deny to implement "allow rm within the project / deny rm outside the project":
Inside project: rm src/foo.py ← You want to allow this
Outside project: rm /tmp/foo ← You want to block this
But deny only checks the "command string prefix", both start the same way
→ Must rely on hooks (Ch 25)
Details of Path Globs
Edit(/Users/amanda/work/openspec/**) → Any file within the project
Edit(/Users/amanda/.ssh/**) → Private key directory
Edit(*.env) → Any .env file (relative path)
→ Globs use ** to match multiple directory levels.
Practical Steps: Switching from default to bypass
- First, run a few times with
defaultto identify operations that are always prompted. - Add "obviously safe" operations to
allow. - Add "obviously dangerous" operations to
deny. - Switch to
bypassPermissions. - Run a test to confirm that disallowed actions are blocked by
deny. - Run production tasks.
Anti-Patterns
❌ Enable bypass without any deny rules
→ This disables all of Claude Code's safety nets.
❌ Overly strict deny rules
→ Legitimate operations are blocked, causing the agent to get stuck.
❌ Attempting to use Bash patterns for path validation
→ Patterns only check strings, not semantics.
→ Use hooks (Ch 25).
❌ Storing tokens in settings.json
→ They should be in settings.local.json + env.
What You Can Do Now
- Configure bypass mode + a strict deny list.
- Explain that deny takes precedence over allow.
- Understand the boundaries of Bash pattern matching (command string prefix).
The next chapter will address "path awareness" issues using PreToolUse hooks.