utils/permissions/ — Permission Engine
Security layer controlling what tools can do and when user approval is required
A 24-file permission system implementing rule-based access control for all tool operations. Manages permission modes (default, plan, acceptEdits, bypassPermissions, auto), path-based filesystem restrictions, shell command classification, dangerous pattern detection, and an AI-powered YOLO classifier for autonomous execution decisions.
📚 What is this & why does it exist?
The permissions module is the security gatekeeper of Claude Code. Every time the AI wants to run a shell command, edit a file, or access the network, it must pass through this system. The engine evaluates rules from 5 sources (user settings, project settings, local settings, CLI args, session), checks for dangerous patterns, and decides: allow silently, ask the user, or deny outright. It prevents the AI from accidentally deleting your git history, running rm -rf /, or accessing files outside your project.
💡 Real-world analogy
Think of airport security. Every passenger (tool action) must go through screening. There's a tiered system: trusted travelers with pre-check (allowlisted tools) walk through quickly, regular passengers (normal tools) go through standard screening (ask mode), and flagged individuals (dangerous patterns) get additional inspection or are denied boarding entirely. The security rules come from multiple authorities (TSA, airline, airport) just like permission rules come from user, project, and policy settings. A killswitch can shut down the fast lane if something goes wrong.
Contents
Architecture
flowchart TD
TOOL["Tool Action Request"] --> PE["permissions.ts\n(Main evaluation engine)"]
PE --> DR["Deny Rules\n(checked first)"]
PE --> AR["Allow Rules"]
PE --> MODE["Permission Mode\n(default/plan/auto/bypass)"]
DR --> FS["filesystem.ts\n(Path validation)"]
DR --> DP["dangerousPatterns.ts\n(Shell patterns)"]
MODE --> YC["yoloClassifier.ts\n(Auto-mode AI classifier)"]
MODE --> BC["bashClassifier.ts\n(Command classification)"]
YC --> DT["denialTracking.ts\n(Circuit breaker)"]
RULES["Rule Sources"] --> PE
RULES --> US["userSettings"]
RULES --> PS["projectSettings"]
RULES --> LS["localSettings"]
RULES --> CLI["cliArg"]
RULES --> SES["session"]
PE --> DECIDE{Decision}
DECIDE --> ALLOW["Allow"]
DECIDE --> ASK["Ask User"]
DECIDE --> DENY["Deny"]
Key Classes & Functions
| Class / Function | File | Purpose |
|---|---|---|
hasPermissionsToUseTool | permissions.ts | Main evaluation: returns PermissionDecision (allow/ask/deny) with reason |
PermissionMode | PermissionMode.ts | Mode enum with display metadata: default, plan, acceptEdits, bypassPermissions, auto |
PermissionRule | PermissionRule.ts | Rule definition: { toolName, ruleContent?, behavior: allow|deny|ask } |
matchingRuleForInput | filesystem.ts | Matches tool input against filesystem path rules with working directory checks |
validatePath | pathValidation.ts | Multi-stage path validation: traversal, shell expansion, tilde, UNC paths |
classifyYoloAction | yoloClassifier.ts | AI classifier determining if tool action is safe for autonomous execution |
parsePermissionRule | shellRuleMatching.ts | Parses shell rules: exact, prefix (legacy npm:*), wildcard (npm * --save) |
transitionPermissionMode | permissionSetup.ts | Handles mode transitions with dangerous permission stripping/restoration |
isDangerousBashPermission | permissionSetup.ts | Detects allow rules matching dangerous patterns (interpreters, sudo, etc.) |
detectUnreachableRules | shadowedRuleDetection.ts | Finds allow rules shadowed by broader ask/deny rules |
applyPermissionUpdate | PermissionUpdate.ts | Applies permission changes: setMode, addRules, removeRules, addDirectories |
loadAllPermissionRulesFromDisk | permissionsLoader.ts | Loads rules from all 3 editable sources: user, project, local settings |
generatePermissionExplanation | permissionExplainer.ts | AI-generated explanations for shell commands with LOW/MEDIUM/HIGH risk levels |
DANGEROUS_BASH_PATTERNS | dangerousPatterns.ts | 42+ dangerous command prefixes: python, node, sudo, ssh, eval, etc. |
Data Structures
| Type | Name | Description |
|---|---|---|
| union | PermissionDecision | PermissionAllowDecision | PermissionAskDecision | PermissionDenyDecision |
| enum | PermissionBehavior | 'allow' | 'deny' | 'ask' — rule behaviors |
| enum | PermissionDecisionReason | 'rule' | 'hook' | 'permissionPromptTool' | 'classifier' | 'safetyCheck' | 'other' |
| enum | PermissionRuleSource | 'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg' |
| union | ShellPermissionRule | Discriminated union: exact, prefix, or wildcard patterns |
| enum | RiskLevel | 'LOW' | 'MEDIUM' | 'HIGH' — permission explainer risk assessment |
| type | DenialTrackingState | Tracks consecutive (max 3) and total (max 20) classifier denials for circuit breaking |
Interactions
| Direction | Partner | Interface | Description |
|---|---|---|---|
| ← | All Tools | hasPermissionsToUseTool() | Every tool action passes through the permission engine before execution |
| → | Settings | loadAllPermissionRulesFromDisk() | Reads rules from user/project/local settings files |
| → | Anthropic API | classifyYoloAction() | Auto-mode classifier calls Claude Haiku for safety assessment |
| → | UI (PromptInput) | cyclePermissionMode() | Shift+Tab cycling updates the active permission mode |
| → | Sandbox | isPathInSandboxWriteAllowlist() | Coordinates with sandbox for path-level write permissions |
| → | Statsig/GrowthBook | checkAndDisableBypassPermissionsIfNeeded() | Feature gate killswitches for bypass and auto modes |
Permission Decision Flow
flowchart TD
REQ["Tool wants to act"] --> DENY_CHECK{"Deny rules match?"}
DENY_CHECK -->|Yes| DENIED["DENY"]
DENY_CHECK -->|No| AGENT_DENY{"Agent-specific deny?"}
AGENT_DENY -->|Yes| DENIED
AGENT_DENY -->|No| ALLOW_CHECK{"Allow rules match?"}
ALLOW_CHECK -->|Yes| ALLOWED["ALLOW"]
ALLOW_CHECK -->|No| MODE{"Permission Mode?"}
MODE -->|bypass| ALLOWED
MODE -->|acceptEdits| EDIT_CHECK{"Is edit/write?"}
EDIT_CHECK -->|Yes| ALLOWED
EDIT_CHECK -->|No| ASK["ASK User"]
MODE -->|auto| CLASSIFIER{"YOLO Classifier"}
CLASSIFIER -->|safe| ALLOWED
CLASSIFIER -->|unsafe| ASK
MODE -->|default/plan| ASK
Configuration
| Knob / Parameter | Default | Description |
|---|---|---|
maxConsecutiveDenials | 3 | Classifier denials before fallback to user prompting |
maxTotalDenials | 20 | Total classifier denials before permanent fallback |
DANGEROUS_FILES | 5 files | .gitconfig, .bashrc, .zshrc, .mcp.json, .claude.json |
DANGEROUS_DIRECTORIES | 4 dirs | .git, .vscode, .idea, .claude |
SAFE_YOLO_ALLOWLISTED_TOOLS | 20+ tools | Read-only tools that skip classifier (FileRead, Grep, Glob, LSP) |
CROSS_PLATFORM_CODE_EXEC | 18 patterns | python, node, deno, tsx, ruby, perl, php, lua, npx, bunx, etc. |
YOLO_CLASSIFIER_TOOL_NAME | "classify_result" | Tool name used by auto-mode classifier responses |
Directory Map
Source tree for permissions
utils/permissions/
├── permissions.ts ← Main evaluation engine: hasPermissionsToUseTool, getAllowRules
├── PermissionMode.ts ← Mode enum with display metadata (title, symbol, color)
├── PermissionResult.ts ← Decision types: Allow/Ask/Deny with reasons
├── PermissionRule.ts ← Rule definition: toolName + ruleContent + behavior
├── PermissionUpdate.ts ← Apply/persist permission changes (addRules, setMode)
├── PermissionUpdateSchema.ts ← Zod schemas for validating permission updates
├── PermissionPromptToolResultSchema.ts ← Schema for external permission prompt tool
├── permissionsLoader.ts ← Load/save rules from disk settings files
├── permissionSetup.ts ← Mode transitions, dangerous permission detection
├── permissionExplainer.ts ← AI-generated command explanations with risk levels
├── permissionRuleParser.ts ← Parse/serialize rules with escape handling
├── pathValidation.ts ← Multi-stage path validation (traversal, expansion, UNC)
├── filesystem.ts ← Filesystem permission checks (read/write by path)
├── shellRuleMatching.ts ← Shell rule matching: exact, prefix, wildcard patterns
├── dangerousPatterns.ts ← 42+ dangerous command patterns (interpreters, sudo, etc.)
├── yoloClassifier.ts ← Auto-mode AI classifier for safe/unsafe decisions
├── bashClassifier.ts ← Bash command classification stub (ANT-only)
├── classifierDecision.ts ← Safe tool allowlist for auto-mode bypass
├── classifierShared.ts ← Shared classifier response parsing utilities
├── autoModeState.ts ← Auto-mode state: active flag, CLI flag, circuit breaker
├── bypassPermissionsKillswitch.ts ← Feature gate killswitches (Statsig/GrowthBook)
├── denialTracking.ts ← Consecutive/total denial tracking with fallback logic
├── shadowedRuleDetection.ts ← Detect unreachable rules shadowed by broader rules
└── getNextPermissionMode.ts ← Mode cycling logic for Shift+Tab
Deep-Dive Highlights
Core mechanisms from this sub-module have been extracted into dedicated focus pages for in-depth exploration.
Deep-Dive Pages
Rule Evaluation Pipeline Focus
How deny-first precedence, 5 rule sources, and tool-specific matching combine to produce Allow/Ask/Deny decisions.
YOLO Auto-Mode Classifier Focus
AI-powered safety classification, denial tracking circuit breaker, and the 20+ tool allowlist for autonomous execution.
Path Validation & Security Focus
Multi-stage path validation, dangerous file/directory lists, working directory enforcement, and shell expansion detection.
Permission Mode Details
| Mode | Symbol | Behavior |
|---|---|---|
default | — | Ask user before any tool execution (safest) |
plan | 📋 | AI can only plan, not execute. Reads allowed, writes denied |
acceptEdits | ✏️ | File edits and writes auto-allowed; shell commands still ask |
bypassPermissions | ⚡ | All tool operations auto-allowed (dangerous) |
auto | 🤖 | AI classifier decides allow/ask per action (feature-gated) |
Rule Source Precedence
Rules are loaded from 5 sources with the following evaluation order (first match wins):
| Priority | Source | Persistence | Typical Use |
|---|---|---|---|
| 1 | session | Current session only | Temporary one-off permissions |
| 2 | cliArg | Current invocation | --allow-tool flags |
| 3 | localSettings | .claude/settings.local.json | Machine-specific overrides (gitignored) |
| 4 | projectSettings | .claude/settings.json | Team-shared project rules (committed) |
| 5 | userSettings | ~/.claude/settings.json | Global user preferences |
Dangerous Pattern Categories
| Category | Examples | Count |
|---|---|---|
| Interpreters | python, node, deno, tsx, ruby, perl, php, lua | 8 |
| Package runners | npx, bunx, npm run, yarn run, pnpm run, bun run | 6 |
| Shells | bash, sh, zsh, fish, eval, exec | 6 |
| Remote commands | ssh, curl, wget | 3 |
| Privilege escalation | sudo, env, xargs | 3 |
| Infrastructure | kubectl, aws, gcloud, gsutil | 4 |
| VCS | git, gh, gh api | 3 |
Code Examples
Example: Permission Rule Matching for Shell Commands
// Shell permission rules support 3 formats:
// 1. Exact: "npm install"
// 2. Prefix (legacy): "npm:*"
// 3. Wildcard: "npm * --save"
export function parsePermissionRule(rule: string): ShellPermissionRule {
if (rule.includes(':*')) {
// Legacy prefix format: "npm:*" matches "npm install", "npm run build"
return { type: 'prefix', prefix: rule.replace(':*', '') };
}
if (hasWildcards(rule)) {
// Wildcard: "git commit -m *" matches any commit message
return { type: 'wildcard', pattern: rule };
}
// Exact match
return { type: 'exact', command: rule };
}
// Wildcard matching with backslash escaping
function matchWildcardPattern(command: string, pattern: string): boolean {
// Convert pattern to regex: * -> .*, \* -> literal *
const regex = pattern
.replace(/([.+?^${}()|[\]\\])/g, '\\$1')
.replace(/(?
Example: Permission Mode Cycling
// Mode cycling on Shift+Tab keypress
export function cyclePermissionMode(
current: PermissionMode,
isAntUser: boolean,
): PermissionMode {
if (isAntUser) {
// Ant users: default → bypassPermissions → auto → default
const cycle: PermissionMode[] = ['default', 'bypassPermissions', 'auto'];
const idx = cycle.indexOf(current);
return cycle[(idx + 1) % cycle.length]!;
}
// External users: default → acceptEdits → plan → bypassPermissions → default
const cycle: PermissionMode[] = [
'default', 'acceptEdits', 'plan', 'bypassPermissions',
];
const idx = cycle.indexOf(current);
return cycle[(idx + 1) % cycle.length]!;
}
Example: Denial Tracking Circuit Breaker
// Tracks consecutive and total classifier denials to prevent loops
const DENIAL_LIMITS = { maxConsecutive: 3, maxTotal: 20 };
export function createDenialTrackingState(): DenialTrackingState {
return { consecutive: 0, total: 0 };
}
export function recordDenial(state: DenialTrackingState): void {
state.consecutive++;
state.total++;
}
export function recordSuccess(state: DenialTrackingState): void {
state.consecutive = 0; // Reset consecutive on any success
}
// When limits are hit, fall back to user prompting
export function shouldFallbackToPrompting(
state: DenialTrackingState,
): boolean {
return (
state.consecutive >= DENIAL_LIMITS.maxConsecutive ||
state.total >= DENIAL_LIMITS.maxTotal
);
}