Chapter 24 | Permission Mode

7 MIN READ | UPDATED: 2026-05-15

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:#c8e6c9

Our 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

  1. First, run a few times with default to identify operations that are always prompted.
  2. Add "obviously safe" operations to allow.
  3. Add "obviously dangerous" operations to deny.
  4. Switch to bypassPermissions.
  5. Run a test to confirm that disallowed actions are blocked by deny.
  6. 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.