How the Permission System Balances Flexibility and Security

AI tool permission management is a dilemma: too strict and efficiency suffers; too loose and security is compromised. Claude Code uses a ten-step state machine, six permission modes, and multi-layered rule sources to build a permission architecture that can run fully automatically while protecting critical resources from tampering. This chapter dissects this tightrope-walking design in depth, helping you understand why certain operations still cannot be bypassed even in `bypassPermissions` mode.

> 💡 **Plain English**: It's like a residential access system—homeowners swipe a card to open the gate automatically, delivery drivers scan a code for verification, and strangers need the homeowner's approval.

### 🌍 Industry Context: Permission and Security Models in AI Coding Tools

Permission management for AI coding tools is an active design problem, and each vendor's solution reflects a different security philosophy:

- **Cursor**: After launching Background Agents, its permission model evolved significantly. Local edits still go through a diff preview that the user accepts or rejects one by one, but cloud-based background agents run in isolated VMs with more permissive execution privileges—their safety is guaranteed by VM-level environmental isolation rather than step-by-step human approval. Additionally, the `.cursor/rules/` `.mdc` conditional rule engine can trigger different permission policies via globs that precisely match specific file types.
- **Aider**: By default, it automatically applies code edits (`--auto-commits`) without permission checks. Users can disable auto-commits with `--no-auto-commits`, but there is no fine-grained, tool-level permission control. Its security philosophy is "trust that the user is running it in the right environment."
- **CodeX (OpenAI)**: Offers three modes—`suggest` (suggest only, no execution), `auto-edit` (auto-edit but commands require confirmation), and `full-auto` (fully automatic)—while implementing OS-level egress rules that replace earlier fragile environment-variable controls. Its security granularity is finer than Aider's but lacks Claude Code's ten-step state machine and safety-immune paths.
- **Windsurf**: Provides permission control through IDE-integrated diff views and terminal confirmations. The Cascade Engine's continuous state awareness tracks the developer's operational trajectory, embedding a certain amount of safety judgment into predictive edits.
- **Google Antigravity**: Enforces strict environmental permission controls through Allow Lists and Deny Lists. Its "Artifacts (intermediate products)" mechanism requires producing a reviewable implementation plan before actually modifying files—a pre-approval security philosophy.
- **Docker sandbox solutions** (CodeX, some Aider configurations): Use container isolation in place of permission checks—the AI can do anything inside the sandbox, but the system outside the sandbox is unaffected. This is a fundamentally different security route from Claude Code's.

Claude Code's permission system makes several unique engineering choices: (1) **safety-immune paths**—even in the highest permission mode, paths like `.git/` and `.claude/` remain protected, which is uncommon among peer tools; (2) **auto mode uses AI to judge permissions**—using a classifier in place of human approval is a controversial but practical innovation; (3) **an eight-layer rule-source priority hierarchy**—supporting enterprise deployment scenarios (MDM policies overriding local settings), which most other tools do not consider. Overall, Claude Code's permission system is among the most complex and fine-grained implementations in its class.

---

## The Problem

Claude Code has a seemingly contradictory design: it has a `bypassPermissions` mode that skips all permission checks, yet certain paths can never be bypassed by any safety mechanism. It must enable automation scenarios to run smoothly while preventing the AI from modifying `.git/` or configuration files. How can both of these coexist?

---

## You Might Think...

You might think the permission system is just a simple "switch"—either permission checks are on or they are all off. Or you might think permissions are single-layered: one master switch controls everything.

In reality, the permission system is a state machine with ten steps. Different "security levels" are handled at different steps, and some steps are even immune to the highest permission mode.

---

## How It Actually Works

### Core: The Ten-Step Permission State Machine

> **[Chart placeholder 2.5-A]**: Ten-step permission state machine flowchart (from "tool call request" to "Allow/Ask/Deny" complete judgment chain, visualizing each step's branch conditions with a flowchart)

The function `hasPermissionsToUseToolInner` is the heart of the permission system. For every tool call, it performs these checks in order:

```
Step 1a: Global deny rule? → Deny immediately
Step 1b: Global ask rule? → Ask (sandbox excepted)
Step 1c: Tool's checkPermissions() → Get tool-level judgment
Step 1d: Tool says deny? → Deny
Step 1e: Tool requires user interaction? → Force ask (bypass mode is ineffective)
Step 1f: Content-level ask rule (e.g., Bash(npm publish:*)) → Force ask (bypass mode is ineffective)
Step 1g: Sensitive paths (.git/, .claude/...) → Force ask (bypass mode is ineffective)
Step 2a: bypassPermissions mode → Allow
Step 2b: Global allow rule → Allow
Step 3:  All other cases → Ask
```

**Key insight: Steps 1e, 1f, and 1g execute before Step 2a (bypass mode).** This is the mechanism that makes "some paths can never be bypassed" a reality—they make their forced decisions before the bypass mode ever takes effect.

> 📚 **Course Connection · Operating Systems / Computer Security**: This ten-step state machine implements a hybrid of **Mandatory Access Control** (MAC) and **Discretionary Access Control** (DAC) from OS security courses. Steps 1e–1g are MAC—security policies enforced by the system that even the resource "owner" (here, the user with bypass privileges) cannot override. Steps 2a–2b are DAC—users can autonomously decide which operations are allowed through configuration rules. This aligns with the design philosophy of Linux's SELinux / AppArmor: MAC policies take precedence over DAC, and the `root` user is still constrained by SELinux policies.

### Safety-Immune Paths (Bypass-Immune)

The "safety-immune" paths protected by Step 1g include:
- `.git/` — git internals; tampering could break version control
- `.claude/` — Claude's own configuration, preventing the AI from modifying its own permissions
- `.vscode/`, `.cursor/`, and other editor configurations
- `~/.bashrc`, `~/.zshrc`, and other shell configurations

This is not accidental—the core idea of this design is: **even in the highest permission mode, the AI should not modify configuration files that "affect the AI's own behavior" without human confirmation.**

### Six Permission Modes

> **[Chart placeholder 2.5-B]**: Six permission modes pyramid (from the strictest `plan` at the top to the most permissive `bypassPermissions` at the bottom, with each layer annotated with applicable scenarios and switching methods)

The system is not a single switch but six modes, each corresponding to a different usage scenario:

| Mode | Behavior | Applicable Scenario |
|------|----------|---------------------|
| `default` | All non-read-only operations require approval | Daily use |
| `plan` | Write operations prohibited (read-only) | Exploration and planning |
| `acceptEdits` | File edits automatically allowed, Bash still requires approval | Rapid coding |
| `bypassPermissions` | Skip all permission checks (except safety immunities) | Full automation |
| `dontAsk` | Convert all "ask" to "deny" | CI / unattended |
| `auto` | AI classifier decides whether to allow | Intelligent automation |

### Auto Mode: Using AI to Judge Whether AI Is Safe

The `auto` mode (ant-only feature) is the most interesting design: when a tool requires permission confirmation, the system does not pop up a human approval dialog. Instead, it launches an **independent AI classifier** that uses the full conversation history as context to judge "should this operation be allowed?"

To save on classifier API call costs, the system designed three **fast paths**:

```
Fast path 1: Simulate acceptEdits mode check
  → If this operation would be allowed under acceptEdits mode → pass directly, skip classifier

Fast path 2: Safe-tool whitelist
  → If the tool is on the safe whitelist → pass directly, skip classifier

↓ Only if neither of the above matches, run the

Classifier (two-phase): classifyYoloAction()
  → Analyze full conversation history
  → shouldBlock=false → allow
  → shouldBlock=true → record denial count
```

### Automatic Fallback: Preventing Infinite Denial Loops

When the AI classifier keeps rejecting an operation, the system proactively falls back to human approval:

- **3 consecutive denials** → Fallback to human approval
- **20 total denials** → Fallback to human approval (`DENIAL_LIMITS.maxConsecutive=3`, `maxTotal=20`)

The logic is: if the classifier keeps saying no, either the task is genuinely problematic or the classifier is misinterpreting it. Both cases are worth a human look rather than wasting tokens in an infinite automatic-denial loop.

### Fail-Closed (Iron Gate)

When the AI classifier API call fails (network error, overload, etc.), the `tengu_iron_gate_closed` feature flag determines behavior:
- Default: **fail-closed** — deny the operation and inform the user to retry later
- When explicitly disabled: fail-open — fall back to standard permission prompts

This is defensive engineering in action: when a critical safety component fails, defaulting to denial is safer than defaulting to allowance. In security engineering, this principle is called **fail-closed**; the counterpart, fail-open, applies to availability-first scenarios (e.g., fire-exit door locks that automatically open when power is lost). Claude Code chooses fail-closed for safety components and allows fail-open degradation for non-safety components, reflecting mature engineering judgment in applying different failure strategies to different components.

---

## Permission Rule Syntax

Permission rules support multi-level, precise control:

```
Bash              — All Bash commands (entire tool)
Bash(git *)       — Only Bash commands starting with git (content match)
Read              — All file reads
mcp__server1      — All tools for a given MCP server (server-level)
Agent(Explore)    — Prohibit Explore-type agents (agent type match)
```

Rules come from eight sources, in descending priority: enterprise policy > feature flags > local settings > user settings > project settings > CLI arguments > commands > session temporary rules.

---

## The Trade-Offs Behind This Design

**The balance point is well chosen:**

The system uses step numbers (1a through 3) to explicitly delineate "what level of permission can override what level of protection." Bypass mode can only affect logic after Step 2a—it cannot affect the hard protections set by Steps 1e, 1f, and 1g. This gives "highest permission" a boundary.

**The cost of complexity:**

Ten steps + six modes + eight rule sources make the total state space of the permission system quite large. For developers, understanding "why am I still being blocked in bypassPermissions mode" requires knowing this complete state machine to debug. This is the inevitable price of trading for flexibility.

**The recursive charm of auto mode:**

AI calls AI to judge whether AI has permission to do something. This is an interesting recursive structure, and it also means that every permission judgment in auto mode may trigger an additional API call (although the three fast paths try their best to avoid this).

---

## What We Can Learn from This

**A permission system should be multi-layered, with different layers of protection having different "breakthrough thresholds."**

Claude Code divides permission checks into three zones:
1. **Absolute safety zone** (Steps 1e–1g): Protection that no mode can bypass
2. **Rules / mode zone** (Steps 2a–2b): Can be overridden by bypass mode or allow rules
3. **Default ask zone** (Step 3): Standard user confirmation flow

This layered design lets the system simultaneously satisfy two seemingly contradictory needs: "full automation" and "always protect critical resources."

---

## Code Landing Points

- `src/utils/permissions/permissions.ts`, line 1158: `hasPermissionsToUseToolInner()`, the complete ten-step state machine
- `src/utils/permissions/permissions.ts`, line 473: `hasPermissionsToUseTool()`, the outer function (handles auto / dontAsk mode logic)
- `src/utils/permissions/PermissionMode.ts` — definitions and metadata for the six modes
- `src/utils/permissions/denialTracking.ts` — denial tracking, `DENIAL_LIMITS = {maxConsecutive: 3, maxTotal: 20}`
- `src/utils/permissions/yoloClassifier.ts` — AI classifier implementation for auto mode
- `src/hooks/toolPermission/` — hook implementations related to tool permissions

---

## Directions for Further Inquiry

- How does the two-phase classifier in auto mode work? What do stage 1 and stage 2 each do?
- How is the safety check for paths like `.git/` implemented at the tool layer? Is it inside FileEditTool's `checkPermissions()`?
- Where is `shouldAvoidPermissionPrompts` set to true? What is its relationship with the `isAsync` parameter?

---

*Quality self-check:*
- [x] Coverage: ten-step state machine, six modes, auto mode, iron gate, and denial tracking all covered
- [x] Fidelity: step numbers come from code comments; numbers come from denialTracking.ts constants
- [x] Readability: intuition built through tables and code blocks; "tightrope" metaphor runs throughout
- [x] Consistency: aligned with global_map.md permission system description
- [x] Criticality: points out the complexity of the state space and the recursive cost of auto mode
- [x] Reusability: linked chapters listed
