Rule System
此内容尚不支持你的语言。
Claude Code’s permission system is driven by rules — declarative statements that say “allow this tool”, “deny that command”, or “always ask before running this”. Rules can come from 8 different sources, each with different persistence characteristics and trust levels. When rules conflict, a deterministic priority system resolves the ambiguity.
Rule Anatomy
Section titled “Rule Anatomy”Every permission rule has three components:
export type PermissionRule = { source: PermissionRuleSource ruleBehavior: PermissionBehavior // 'allow' | 'deny' | 'ask' ruleValue: PermissionRuleValue}
export type PermissionRuleValue = { toolName: string // "Bash", "Edit", "mcp__server__tool" ruleContent?: string // Optional: "npm install:*", "git commit"}Rule String Format
Section titled “Rule String Format”Rules are persisted as strings in settings files and parsed by permissionRuleParser.ts:
export function permissionRuleValueFromString(ruleString: string): PermissionRuleValue { // Find the first unescaped opening parenthesis const openParenIndex = findFirstUnescapedChar(ruleString, '(') if (openParenIndex === -1) { return { toolName: normalizeLegacyToolName(ruleString) } } // Extract tool name and content const toolName = ruleString.substring(0, openParenIndex) const rawContent = ruleString.substring(openParenIndex + 1, closeParenIndex) // Empty content or standalone wildcard → tool-wide rule if (rawContent === '' || rawContent === '*') { return { toolName: normalizeLegacyToolName(toolName) } } return { toolName, ruleContent: unescapeRuleContent(rawContent) }}Parentheses in rule content are escaped with backslashes:
// Bash(python -c "print(1)") is stored as:// Bash(python -c "print\(1\)")export function escapeRuleContent(content: string): string { return content .replace(/\\/g, '\\\\') // Escape backslashes first .replace(/\(/g, '\\(') // Escape opening parentheses .replace(/\)/g, '\\)') // Escape closing parentheses}Rule Matching Modes
Section titled “Rule Matching Modes”Rules match against commands in three modes, implemented via ShellPermissionRule:
| Mode | Syntax | Example | Matches |
|---|---|---|---|
| Exact | command | Bash(npm install express) | Only npm install express |
| Prefix | command:* | Bash(npm install:*) | npm install express, npm install -D jest, etc. |
| Wildcard | pattern*pattern | Bash(*test*) | Any command containing “test” |
export function parsePermissionRule(rule: string): ShellPermissionRule { // Check for :* suffix → prefix rule // Check for * → wildcard rule // Otherwise → exact match}The 8 Rule Sources
Section titled “The 8 Rule Sources”Rules originate from 8 sources, defined across two type systems:
export const SETTING_SOURCES = [ 'userSettings', // 1. User settings (global) 'projectSettings', // 2. Project settings (shared) 'localSettings', // 3. Local settings (gitignored) 'flagSettings', // 4. Flag settings (--settings flag) 'policySettings', // 5. Policy settings (managed/enterprise)] as const
// src/types/permissions.ts — additional rule-specific sourcesexport type PermissionRuleSource = | SettingSource // All 5 above | 'cliArg' // 6. CLI arguments | 'command' // 7. In-session commands | 'session' // 8. User approvals during sessionSource 1: User Settings (userSettings)
Section titled “Source 1: User Settings (userSettings)”Location: ~/.claude/settings.json
Scope: Global — applies to all projects
Persistence: Permanent until manually edited
Display name: “user settings”
{ "permissions": { "allow": ["Edit", "Bash(git:*)"], "deny": ["Bash(rm -rf:*)"], "ask": [] }}Source 2: Project Settings (projectSettings)
Section titled “Source 2: Project Settings (projectSettings)”Location: .claude/settings.json in project root
Scope: This project — shared across team
Persistence: Committed to version control
Display name: “shared project settings”
Teams use this to define baseline rules for the project:
{ "permissions": { "allow": ["Bash(npm test:*)", "Bash(npm run lint:*)"], "deny": ["Bash(npm publish:*)"] }}Source 3: Local Settings (localSettings)
Section titled “Source 3: Local Settings (localSettings)”Location: .claude/settings.local.json in project root
Scope: This project, this machine only
Persistence: Gitignored — not shared
Display name: “project local settings”
This is where user approvals from “always allow” prompts are typically saved:
{ "permissions": { "allow": ["Bash(python3:*)", "Bash(cargo build:*)"] }}Source 4: Flag Settings (flagSettings)
Section titled “Source 4: Flag Settings (flagSettings)”Location: File path specified by --settings CLI flag
Scope: This session
Persistence: As long as the file exists
Display name: “command line arguments”
claude --settings /path/to/custom-settings.jsonSource 5: Policy Settings (policySettings)
Section titled “Source 5: Policy Settings (policySettings)”Location: managed-settings.json or remote API
Scope: Enterprise-wide
Persistence: Managed by IT
Display name: “enterprise managed settings”
When allowManagedPermissionRulesOnly is enabled, this source overrides all others:
export function loadAllPermissionRulesFromDisk(): PermissionRule[] { if (shouldAllowManagedPermissionRulesOnly()) { return getPermissionRulesForSource('policySettings') } // Otherwise, load from all enabled sources}Source 6: CLI Arguments (cliArg)
Section titled “Source 6: CLI Arguments (cliArg)”Scope: This session Persistence: Command-line only
claude --allowed-tools "Bash(git:*),Edit"claude --denied-tools "Bash(curl:*)"Source 7: In-Session Commands (command)
Section titled “Source 7: In-Session Commands (command)”Scope: This session Persistence: Memory only
> /allowed-tools Bash(npm test:*)Source 8: Session Approvals (session)
Section titled “Source 8: Session Approvals (session)”Scope: This session Persistence: Memory only (lost on exit)
When a user approves a permission prompt with “Yes, allow this once”, the rule is stored in the session source.
Priority and Merge Algorithm
Section titled “Priority and Merge Algorithm”Behavior Priority: Deny > Ask > Allow
Section titled “Behavior Priority: Deny > Ask > Allow”When the same command matches rules with different behaviors, the priority is always:
- Deny — checked first, takes immediate effect
- Ask — checked second, triggers permission prompt
- Allow — checked last, auto-approves
This is enforced throughout the codebase:
// bashToolCheckPermission (src/tools/BashTool/bashPermissions.ts)// 1. Exact match first (deny > ask > allow)// 2. Prefix/wildcard deny → DENY// 3. Prefix/wildcard ask → ASK// 4. Path constraints// 5. Prefix/wildcard allow → ALLOWSource Priority for Same-Behavior Rules
Section titled “Source Priority for Same-Behavior Rules”Within the same behavior (e.g., two allow rules from different sources), the first match wins. Rules are collected from all sources in order:
const PERMISSION_RULE_SOURCES = [ ...SETTING_SOURCES, // userSettings, projectSettings, localSettings, flagSettings, policySettings 'cliArg', 'command', 'session',] as const satisfies readonly PermissionRuleSource[]The SETTING_SOURCES order is defined in constants.ts:
// src/utils/settings/constants.ts (line 7)export const SETTING_SOURCES = [ 'userSettings', // First (lowest priority for settings, but rules use first-match) 'projectSettings', 'localSettings', 'flagSettings', 'policySettings', // Last (highest trust)] as constRule Collection Flow
Section titled “Rule Collection Flow”flowchart TD A[Tool invocation] --> B[Collect ALL rules from ALL sources]
B --> DenyRules["Deny Rules\n(all sources merged)"] B --> AskRules["Ask Rules\n(all sources merged)"] B --> AllowRules["Allow Rules\n(all sources merged)"]
DenyRules --> D1{Any deny\nmatches?} D1 -->|Yes| DENY[DENY] D1 -->|No| A1
AskRules --> A1{Any ask\nmatches?} A1 -->|Yes| ASK[ASK] A1 -->|No| L1
AllowRules --> L1{Any allow\nmatches?} L1 -->|Yes| ALLOW[ALLOW] L1 -->|No| PASSTHROUGH[No rule matched\nfallback to mode]Non-Bypassable Safety Checks
Section titled “Non-Bypassable Safety Checks”Some permission checks cannot be overridden by any rule or mode. These are the system’s ultimate safety net:
1. Safety Check on Sensitive Paths
Section titled “1. Safety Check on Sensitive Paths”Files in .git/, .claude/, .vscode/, and shell configuration files (.bashrc, .zshrc, .profile) always trigger a permission prompt, even in bypassPermissions mode:
// src/utils/permissions/permissions.ts (step 1g)if (toolPermissionResult?.behavior === 'ask' && toolPermissionResult.decisionReason?.type === 'safetyCheck') { return toolPermissionResult // Cannot be bypassed}2. Content-Specific Ask Rules
Section titled “2. Content-Specific Ask Rules”When a user explicitly configures an ask rule with content (e.g., Bash(npm publish:*)), this survives even in bypass mode:
// Step 1f: Content-specific ask rulesif (toolPermissionResult?.behavior === 'ask' && toolPermissionResult.decisionReason?.type === 'rule' && toolPermissionResult.decisionReason.rule.ruleBehavior === 'ask') { return toolPermissionResult // Respected even in bypass mode}3. Dangerous Pattern Stripping at Auto Mode Entry
Section titled “3. Dangerous Pattern Stripping at Auto Mode Entry”When transitioning to auto mode, dangerous allow rules are proactively removed:
export function isDangerousBashPermission(toolName: string, ruleContent?: string): boolean { // Bash (no content) = allow all → DANGEROUS // Bash(python:*) = allow interpreter → DANGEROUS // Bash(git commit:*) = specific command → SAFE}Dangerous patterns include:
- Tool-level allow (
Bashwith no content) - Interpreter prefixes:
python:*,node:*,ruby:*,perl:*, etc. - Eval-equivalents:
eval:*,exec:*,env:*,xargs:* - Shell launchers:
bash:*,sh:*,zsh:* - Remote execution:
ssh:* - Privilege escalation:
sudo:*
Rule Format in Settings Files
Section titled “Rule Format in Settings Files”Rules are stored in the permissions key of settings JSON files:
{ "$schema": "https://json.schemastore.org/claude-code-settings.json", "permissions": { "allow": [ "Edit", "Bash(git:*)", "Bash(npm test:*)", "Bash(npm run lint:*)", "mcp__server1" ], "deny": [ "Bash(rm -rf:*)", "Bash(npm publish:*)", "Bash(curl:*)" ], "ask": [ "Bash(docker:*)" ] }}MCP Server Rules
Section titled “MCP Server Rules”MCP (Model Context Protocol) tools support server-level rules:
// src/utils/permissions/permissions.ts (line ~259)// rule "mcp__server1" matches tool "mcp__server1__tool1"const ruleInfo = mcpInfoFromString(rule.ruleValue.toolName)const toolInfo = mcpInfoFromString(nameForRuleMatch)return ruleInfo !== null && toolInfo !== null && (ruleInfo.toolName === undefined || ruleInfo.toolName === '*') && ruleInfo.serverName === toolInfo.serverName| Rule | Matches |
|---|---|
mcp__server1 | All tools from server1 |
mcp__server1__* | All tools from server1 (wildcard variant) |
mcp__server1__read | Only the read tool from server1 |
Legacy Tool Name Aliases
Section titled “Legacy Tool Name Aliases”Tool names are normalized for backward compatibility:
const LEGACY_TOOL_NAME_ALIASES: Record<string, string> = { Task: AGENT_TOOL_NAME, // "Task" → "Agent" KillShell: TASK_STOP_TOOL_NAME, // "KillShell" → "TaskStop" AgentOutputTool: TASK_OUTPUT_TOOL_NAME, BashOutputTool: TASK_OUTPUT_TOOL_NAME,}Rule Persistence and Updates
Section titled “Rule Persistence and Updates”Permission Update Operations
Section titled “Permission Update Operations”When rules change (user approval, settings edit, CLI command), updates flow through a typed operation system:
export type PermissionUpdate = | { type: 'addRules'; destination: PermissionUpdateDestination; rules: PermissionRuleValue[]; behavior: PermissionBehavior } | { type: 'replaceRules'; destination: PermissionUpdateDestination; rules: PermissionRuleValue[]; behavior: PermissionBehavior } | { type: 'removeRules'; destination: PermissionUpdateDestination; rules: PermissionRuleValue[]; behavior: PermissionBehavior } | { type: 'setMode'; destination: PermissionUpdateDestination; mode: ExternalPermissionMode } | { type: 'addDirectories'; destination: PermissionUpdateDestination; directories: string[] } | { type: 'removeDirectories'; destination: PermissionUpdateDestination; directories: string[] }Disk Sync Algorithm
Section titled “Disk Sync Algorithm”When settings files change on disk (hot reload), the system performs a full replacement sync:
export function syncPermissionRulesFromDisk( toolPermissionContext: ToolPermissionContext, rules: PermissionRule[],): ToolPermissionContext { let context = toolPermissionContext
// When allowManagedPermissionRulesOnly, clear all non-policy sources if (shouldAllowManagedPermissionRulesOnly()) { for (const source of sourcesToClear) { for (const behavior of behaviors) { context = applyPermissionUpdate(context, { type: 'replaceRules', rules: [], behavior, destination: source, }) } } }
// Clear ALL disk-based sources before applying new rules // This ensures deleted rules are actually removed for (const diskSource of ['userSettings', 'projectSettings', 'localSettings']) { for (const behavior of ['allow', 'deny', 'ask']) { context = applyPermissionUpdate(context, { type: 'replaceRules', rules: [], behavior, destination: diskSource, }) } }
// Apply new rules const updates = convertRulesToUpdates(rules, 'replaceRules') return applyPermissionUpdates(context, updates)}Deleting Rules
Section titled “Deleting Rules”Rules can be deleted from editable sources (user, project, local) but not from read-only sources (policy, flag):
export async function deletePermissionRule({ rule, ... }): Promise<void> { if (rule.source === 'policySettings' || rule.source === 'flagSettings' || rule.source === 'command') { throw new Error('Cannot delete permission rules from read-only settings') } // Update in-memory context const updatedContext = applyPermissionUpdate(initialContext, { type: 'removeRules', rules: [rule.ruleValue], behavior: rule.ruleBehavior, destination: rule.source as PermissionUpdateDestination, }) // Persist to disk for file-based sources switch (destination) { case 'localSettings': case 'userSettings': case 'projectSettings': deletePermissionRuleFromSettings(rule) break case 'cliArg': case 'session': break // In-memory only, no disk persistence }}Common Rule Configurations
Section titled “Common Rule Configurations”Development Team Baseline
Section titled “Development Team Baseline”{ "permissions": { "allow": [ "Edit", "Bash(git:*)", "Bash(npm test:*)", "Bash(npm run:*)", "Bash(npx:*)" ], "deny": [ "Bash(npm publish:*)", "Bash(rm -rf:*)" ] }}Enterprise Lockdown
Section titled “Enterprise Lockdown”{ "allowManagedPermissionRulesOnly": true, "permissions": { "allow": [ "Bash(git status)", "Bash(git diff:*)", "Bash(git log:*)" ], "deny": [ "Bash(curl:*)", "Bash(wget:*)", "Bash(ssh:*)" ] }}Individual Developer Override
Section titled “Individual Developer Override”In .claude/settings.local.json (gitignored):
{ "permissions": { "allow": [ "Bash(docker compose:*)", "Bash(python3:*)" ] }}Setting Sources and Scope Control
Section titled “Setting Sources and Scope Control”The --setting-sources flag controls which sources are loaded:
export function parseSettingSourcesFlag(flag: string): SettingSource[] { // "user,project,local" → ['userSettings', 'projectSettings', 'localSettings']}
export function getEnabledSettingSources(): SettingSource[] { const allowed = getAllowedSettingSources() // Always include policy and flag settings const result = new Set<SettingSource>(allowed) result.add('policySettings') result.add('flagSettings') return Array.from(result)}Policy and flag settings are always loaded, regardless of the --setting-sources flag. This ensures enterprise policies cannot be bypassed by local configuration.
Summary
Section titled “Summary”The rule system achieves its security goals through:
- Behavior priority (deny > ask > allow) ensures the most restrictive rule always wins
- Source diversity (8 sources) supports individual, team, and enterprise workflows
- Non-bypassable checks protect critical system paths even in full-trust modes
- Dangerous pattern detection prevents overly broad rules from undermining auto mode
- Hot reload keeps rules current without restarting the session
- Legacy compatibility normalizes old tool names seamlessly