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.

24
Files
6
Modes
5
Sources
42+
Dangerous Patterns

📚 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 / FunctionFilePurpose
hasPermissionsToUseToolpermissions.tsMain evaluation: returns PermissionDecision (allow/ask/deny) with reason
PermissionModePermissionMode.tsMode enum with display metadata: default, plan, acceptEdits, bypassPermissions, auto
PermissionRulePermissionRule.tsRule definition: { toolName, ruleContent?, behavior: allow|deny|ask }
matchingRuleForInputfilesystem.tsMatches tool input against filesystem path rules with working directory checks
validatePathpathValidation.tsMulti-stage path validation: traversal, shell expansion, tilde, UNC paths
classifyYoloActionyoloClassifier.tsAI classifier determining if tool action is safe for autonomous execution
parsePermissionRuleshellRuleMatching.tsParses shell rules: exact, prefix (legacy npm:*), wildcard (npm * --save)
transitionPermissionModepermissionSetup.tsHandles mode transitions with dangerous permission stripping/restoration
isDangerousBashPermissionpermissionSetup.tsDetects allow rules matching dangerous patterns (interpreters, sudo, etc.)
detectUnreachableRulesshadowedRuleDetection.tsFinds allow rules shadowed by broader ask/deny rules
applyPermissionUpdatePermissionUpdate.tsApplies permission changes: setMode, addRules, removeRules, addDirectories
loadAllPermissionRulesFromDiskpermissionsLoader.tsLoads rules from all 3 editable sources: user, project, local settings
generatePermissionExplanationpermissionExplainer.tsAI-generated explanations for shell commands with LOW/MEDIUM/HIGH risk levels
DANGEROUS_BASH_PATTERNSdangerousPatterns.ts42+ dangerous command prefixes: python, node, sudo, ssh, eval, etc.

Data Structures

TypeNameDescription
unionPermissionDecisionPermissionAllowDecision | PermissionAskDecision | PermissionDenyDecision
enumPermissionBehavior'allow' | 'deny' | 'ask' — rule behaviors
enumPermissionDecisionReason'rule' | 'hook' | 'permissionPromptTool' | 'classifier' | 'safetyCheck' | 'other'
enumPermissionRuleSource'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg'
unionShellPermissionRuleDiscriminated union: exact, prefix, or wildcard patterns
enumRiskLevel'LOW' | 'MEDIUM' | 'HIGH' — permission explainer risk assessment
typeDenialTrackingStateTracks consecutive (max 3) and total (max 20) classifier denials for circuit breaking

Interactions

DirectionPartnerInterfaceDescription
←All ToolshasPermissionsToUseTool()Every tool action passes through the permission engine before execution
→SettingsloadAllPermissionRulesFromDisk()Reads rules from user/project/local settings files
→Anthropic APIclassifyYoloAction()Auto-mode classifier calls Claude Haiku for safety assessment
→UI (PromptInput)cyclePermissionMode()Shift+Tab cycling updates the active permission mode
→SandboxisPathInSandboxWriteAllowlist()Coordinates with sandbox for path-level write permissions
→Statsig/GrowthBookcheckAndDisableBypassPermissionsIfNeeded()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 / ParameterDefaultDescription
maxConsecutiveDenials3Classifier denials before fallback to user prompting
maxTotalDenials20Total classifier denials before permanent fallback
DANGEROUS_FILES5 files.gitconfig, .bashrc, .zshrc, .mcp.json, .claude.json
DANGEROUS_DIRECTORIES4 dirs.git, .vscode, .idea, .claude
SAFE_YOLO_ALLOWLISTED_TOOLS20+ toolsRead-only tools that skip classifier (FileRead, Grep, Glob, LSP)
CROSS_PLATFORM_CODE_EXEC18 patternspython, 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.

Extracted topic page

YOLO Auto-Mode Classifier Focus

AI-powered safety classification, denial tracking circuit breaker, and the 20+ tool allowlist for autonomous execution.

Extracted topic page

Path Validation & Security Focus

Multi-stage path validation, dangerous file/directory lists, working directory enforcement, and shell expansion detection.

Extracted topic page

Permission Mode Details

ModeSymbolBehavior
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):

PrioritySourcePersistenceTypical Use
1sessionCurrent session onlyTemporary one-off permissions
2cliArgCurrent invocation--allow-tool flags
3localSettings.claude/settings.local.jsonMachine-specific overrides (gitignored)
4projectSettings.claude/settings.jsonTeam-shared project rules (committed)
5userSettings~/.claude/settings.jsonGlobal user preferences

Dangerous Pattern Categories

CategoryExamplesCount
Interpreterspython, node, deno, tsx, ruby, perl, php, lua8
Package runnersnpx, bunx, npm run, yarn run, pnpm run, bun run6
Shellsbash, sh, zsh, fish, eval, exec6
Remote commandsssh, curl, wget3
Privilege escalationsudo, env, xargs3
Infrastructurekubectl, aws, gcloud, gsutil4
VCSgit, gh, gh api3

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
  );
}