跳转到内容

Rule System

Claude Code 的 permission 系统由 rule 驱动——这些声明式语句表达”允许此 tool”、“拒绝该命令”或”运行前始终询问”。rule 可来自 8 个不同来源,各自具有不同的持久化特性和信任级别。当 rule 冲突时,确定性优先级系统会解决歧义。

每条 permission rule 包含三个组件:

src/types/permissions.ts
export type PermissionRule = {
source: PermissionRuleSource
ruleBehavior: PermissionBehavior // 'allow' | 'deny' | 'ask'
ruleValue: PermissionRuleValue
}
export type PermissionRuleValue = {
toolName: string // "Bash"、"Edit"、"mcp__server__tool"
ruleContent?: string // 可选:"npm install:*"、"git commit"
}

rule 以字符串形式持久化在 settings 文件中,由 permissionRuleParser.ts 解析:

src/utils/permissions/permissionRuleParser.ts
export function permissionRuleValueFromString(ruleString: string): PermissionRuleValue {
// 查找第一个未转义的左括号
const openParenIndex = findFirstUnescapedChar(ruleString, '(')
if (openParenIndex === -1) {
return { toolName: normalizeLegacyToolName(ruleString) }
}
// 提取 tool 名称和内容
const toolName = ruleString.substring(0, openParenIndex)
const rawContent = ruleString.substring(openParenIndex + 1, closeParenIndex)
// 空内容或单独的通配符 → tool 级别规则
if (rawContent === '' || rawContent === '*') {
return { toolName: normalizeLegacyToolName(toolName) }
}
return { toolName, ruleContent: unescapeRuleContent(rawContent) }
}

rule 内容中的括号用反斜杠转义:

// Bash(python -c "print(1)") 存储为:
// Bash(python -c "print\(1\)")
export function escapeRuleContent(content: string): string {
return content
.replace(/\\/g, '\\\\') // 先转义反斜杠
.replace(/\(/g, '\\(') // 转义左括号
.replace(/\)/g, '\\)') // 转义右括号
}

rule 通过 ShellPermissionRule 以三种模式匹配命令:

模式语法示例匹配内容
精确匹配commandBash(npm install express)npm install express
前缀匹配command:*Bash(npm install:*)npm install expressnpm install -D jest
通配符pattern*patternBash(*test*)任意包含 “test” 的命令
src/utils/permissions/shellRuleMatching.ts
export function parsePermissionRule(rule: string): ShellPermissionRule {
// 检查 :* 后缀 → 前缀规则
// 检查 * → 通配符规则
// 否则 → 精确匹配
}

rule 来自 8 个来源,分布在两个类型系统中:

src/utils/settings/constants.ts
export const SETTING_SOURCES = [
'userSettings', // 1. 用户 settings(全局)
'projectSettings', // 2. 项目 settings(共享)
'localSettings', // 3. 本地 settings(gitignored)
'flagSettings', // 4. Flag settings(--settings flag)
'policySettings', // 5. Policy settings(受管理/企业)
] as const
// src/types/permissions.ts — 额外的规则特定来源
export type PermissionRuleSource =
| SettingSource // 以上全部 5 个
| 'cliArg' // 6. CLI 参数
| 'command' // 7. 会话内命令
| 'session' // 8. 会话期间用户批准

来源 1:用户 Settings(userSettings

Section titled “来源 1:用户 Settings(userSettings)”

位置~/.claude/settings.json 范围:全局——适用于所有项目 持久性:永久,直到手动编辑 显示名称:“user settings”

{
"permissions": {
"allow": ["Edit", "Bash(git:*)"],
"deny": ["Bash(rm -rf:*)"],
"ask": []
}
}

来源 2:项目 Settings(projectSettings

Section titled “来源 2:项目 Settings(projectSettings)”

位置:项目根目录的 .claude/settings.json 范围:当前项目——跨团队共享 持久性:提交到版本控制 显示名称:“shared project settings”

团队使用此文件为项目定义基准规则:

{
"permissions": {
"allow": ["Bash(npm test:*)", "Bash(npm run lint:*)"],
"deny": ["Bash(npm publish:*)"]
}
}

来源 3:本地 Settings(localSettings

Section titled “来源 3:本地 Settings(localSettings)”

位置:项目根目录的 .claude/settings.local.json 范围:当前项目,仅限本机 持久性:gitignored——不共享 显示名称:“project local settings”

“始终允许” prompt 中的用户批准通常保存在此处:

{
"permissions": {
"allow": ["Bash(python3:*)", "Bash(cargo build:*)"]
}
}

来源 4:Flag Settings(flagSettings

Section titled “来源 4:Flag Settings(flagSettings)”

位置--settings CLI flag 指定的文件路径 范围:当前会话 持久性:文件存在期间有效 显示名称:“command line arguments”

Terminal window
claude --settings /path/to/custom-settings.json

来源 5:Policy Settings(policySettings

Section titled “来源 5:Policy Settings(policySettings)”

位置managed-settings.json 或远程 API 范围:企业级 持久性:由 IT 管理 显示名称:“enterprise managed settings”

allowManagedPermissionRulesOnly 启用时,此来源覆盖所有其他来源:

src/utils/permissions/permissionsLoader.ts
export function loadAllPermissionRulesFromDisk(): PermissionRule[] {
if (shouldAllowManagedPermissionRulesOnly()) {
return getPermissionRulesForSource('policySettings')
}
// 否则,从所有启用的来源加载
}

范围:当前会话 持久性:仅命令行

Terminal window
claude --allowed-tools "Bash(git:*),Edit"
claude --denied-tools "Bash(curl:*)"

范围:当前会话 持久性:仅内存

> /allowed-tools Bash(npm test:*)

范围:当前会话 持久性:仅内存(退出后丢失)

当用户以”是,本次允许”批准 permission prompt 时,rule 存储在 session 来源中。

当同一命令匹配到不同 behavior 的 rule 时,优先级始终为:

  1. Deny — 最先检查,立即生效
  2. Ask — 次之检查,触发 permission prompt
  3. Allow — 最后检查,自动批准

整个代码库中均强制执行此规则:

// bashToolCheckPermission (src/tools/BashTool/bashPermissions.ts)
// 1. 先精确匹配(deny > ask > allow)
// 2. 前缀/通配符 deny → DENY
// 3. 前缀/通配符 ask → ASK
// 4. 路径约束
// 5. 前缀/通配符 allow → ALLOW

在相同 behavior 中(例如两条来自不同来源的 allow rule),先匹配者获胜。rule 按顺序从所有来源收集:

src/utils/permissions/permissions.ts
const PERMISSION_RULE_SOURCES = [
...SETTING_SOURCES, // userSettings、projectSettings、localSettings、flagSettings、policySettings
'cliArg',
'command',
'session',
] as const satisfies readonly PermissionRuleSource[]

SETTING_SOURCES 的顺序在 constants.ts 中定义:

// src/utils/settings/constants.ts (line 7)
export const SETTING_SOURCES = [
'userSettings', // 第一个(settings 优先级最低,但 rule 使用先匹配)
'projectSettings',
'localSettings',
'flagSettings',
'policySettings', // 最后(信任度最高)
] as const
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]

某些 permission 检查不能被任何 rule 或 mode 覆盖。这些是系统的最终安全网:

.git/.claude/.vscode/ 中的文件以及 shell 配置文件(.bashrc.zshrc.profile)始终触发 permission prompt,即使在 bypassPermissions 模式下也如此:

// src/utils/permissions/permissions.ts (step 1g)
if (toolPermissionResult?.behavior === 'ask' &&
toolPermissionResult.decisionReason?.type === 'safetyCheck') {
return toolPermissionResult // 不可绕过
}

当用户显式配置含内容的 ask rule(如 Bash(npm publish:*))时,即使在 bypass 模式下也会生效:

// Step 1f:内容特定的 ask rule
if (toolPermissionResult?.behavior === 'ask' &&
toolPermissionResult.decisionReason?.type === 'rule' &&
toolPermissionResult.decisionReason.rule.ruleBehavior === 'ask') {
return toolPermissionResult // 即使在 bypass 模式下也会遵守
}

3. 进入 Auto Mode 时的危险 Pattern 剥离

Section titled “3. 进入 Auto Mode 时的危险 Pattern 剥离”

切换到 auto mode 时,危险 allow rule 会被主动移除:

src/utils/permissions/permissionSetup.ts
export function isDangerousBashPermission(toolName: string, ruleContent?: string): boolean {
// Bash(无内容)= 全部允许 → 危险
// Bash(python:*) = 允许解释器 → 危险
// Bash(git commit:*) = 特定命令 → 安全
}

危险 pattern 包括:

  • Tool 级别 allow(无内容的 Bash
  • 解释器前缀:python:*node:*ruby:*perl:*
  • Eval 等价命令:eval:*exec:*env:*xargs:*
  • Shell 启动器:bash:*sh:*zsh:*
  • 远程执行:ssh:*
  • 权限提升:sudo:*

rule 存储在 settings JSON 文件的 permissions 键中:

{
"$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(Model Context Protocol)tool 支持 server 级别规则:

// src/utils/permissions/permissions.ts (line ~259)
// rule "mcp__server1" 匹配 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匹配内容
mcp__server1server1 的所有 tool
mcp__server1__*server1 的所有 tool(通配符变体)
mcp__server1__readserver1read tool

tool 名称经过标准化以保持向后兼容:

src/utils/permissions/permissionRuleParser.ts
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 发生变化(用户批准、settings 编辑、CLI 命令)时,更新通过类型化操作系统流转:

src/types/permissions.ts
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[] }

当 settings 文件在磁盘上发生变化(热重载)时,系统执行完整替换同步:

src/utils/permissions/permissions.ts
export function syncPermissionRulesFromDisk(
toolPermissionContext: ToolPermissionContext,
rules: PermissionRule[],
): ToolPermissionContext {
let context = toolPermissionContext
// 当 allowManagedPermissionRulesOnly 时,清除所有非 policy 来源
if (shouldAllowManagedPermissionRulesOnly()) {
for (const source of sourcesToClear) {
for (const behavior of behaviors) {
context = applyPermissionUpdate(context, {
type: 'replaceRules', rules: [], behavior, destination: source,
})
}
}
}
// 应用新规则前清除所有基于磁盘的来源
// 这确保已删除的 rule 被真正移除
for (const diskSource of ['userSettings', 'projectSettings', 'localSettings']) {
for (const behavior of ['allow', 'deny', 'ask']) {
context = applyPermissionUpdate(context, {
type: 'replaceRules', rules: [], behavior, destination: diskSource,
})
}
}
// 应用新规则
const updates = convertRulesToUpdates(rules, 'replaceRules')
return applyPermissionUpdates(context, updates)
}

rule 可从可编辑来源(用户、项目、本地)删除,但不能从只读来源(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')
}
// 更新内存中的 context
const updatedContext = applyPermissionUpdate(initialContext, {
type: 'removeRules', rules: [rule.ruleValue], behavior: rule.ruleBehavior,
destination: rule.source as PermissionUpdateDestination,
})
// 对基于文件的来源持久化到磁盘
switch (destination) {
case 'localSettings':
case 'userSettings':
case 'projectSettings':
deletePermissionRuleFromSettings(rule)
break
case 'cliArg':
case 'session':
break // 仅内存,无磁盘持久化
}
}
{
"permissions": {
"allow": [
"Edit",
"Bash(git:*)",
"Bash(npm test:*)",
"Bash(npm run:*)",
"Bash(npx:*)"
],
"deny": [
"Bash(npm publish:*)",
"Bash(rm -rf:*)"
]
}
}
{
"allowManagedPermissionRulesOnly": true,
"permissions": {
"allow": [
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)"
],
"deny": [
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(ssh:*)"
]
}
}

.claude/settings.local.json(gitignored)中:

{
"permissions": {
"allow": [
"Bash(docker compose:*)",
"Bash(python3:*)"
]
}
}

--setting-sources flag 控制加载哪些来源:

src/utils/settings/constants.ts
export function parseSettingSourcesFlag(flag: string): SettingSource[] {
// "user,project,local" → ['userSettings', 'projectSettings', 'localSettings']
}
export function getEnabledSettingSources(): SettingSource[] {
const allowed = getAllowedSettingSources()
// 始终包含 policy 和 flag settings
const result = new Set<SettingSource>(allowed)
result.add('policySettings')
result.add('flagSettings')
return Array.from(result)
}

Policy 和 flag settings 始终加载,不受 --setting-sources flag 影响。这确保企业策略无法被本地配置绕过。

rule 系统通过以下方式实现安全目标:

  1. Behavior 优先级(deny > ask > allow)确保最严格的规则始终胜出
  2. 来源多样性(8 种来源)支持个人、团队和企业工作流
  3. 不可绕过的检查即使在完全信任模式下也保护关键系统路径
  4. 危险 pattern 检测防止过宽规则破坏 auto mode
  5. 热重载无需重启会话即可保持规则最新
  6. Legacy 兼容无缝标准化旧版 tool 名称