跳转到内容

Permission Model

Claude Code 实现了所有 AI 编程工具中最精密的 permission 系统之一。每一次 tool 调用——无论是编辑文件、执行 bash 命令还是启动 sub-agent——都要经过多层 permission 决策管道。本章从类型定义到运行时决策,全面剖析完整的 permission model。

permission 系统分布在以下几个关键文件中:

文件用途
src/types/permissions.ts核心类型定义、mode 枚举、决策类型
src/utils/permissions/PermissionMode.tsMode 配置、显示名称、符号
src/utils/permissions/permissions.ts主要权限检查逻辑(hasPermissionsToUseTool
src/utils/permissions/permissionsLoader.ts从磁盘加载规则(settings 文件)
src/utils/permissions/permissionSetup.ts初始 permission context 设置、危险规则检测
src/utils/permissions/permissionRuleParser.tsrule 字符串解析(Bash(npm install:*) → 结构化值)

每个 Claude Code 会话在任意时刻都运行在唯一一种 permission mode 下。该 mode 决定了没有显式规则匹配时的基准行为。

src/types/permissions.ts
export const EXTERNAL_PERMISSION_MODES = [
'acceptEdits',
'bypassPermissions',
'default',
'dontAsk',
'plan',
] as const
export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'

标准模式。只读操作自动放行,写操作和 bash 命令若没有显式 allow rule 覆盖,则向用户触发 permission prompt。

src/utils/permissions/PermissionMode.ts
default: {
title: 'Default',
shortTitle: 'Default',
symbol: '',
color: 'text',
external: 'default',
},

2. acceptEdits — 自动接受文件编辑

Section titled “2. acceptEdits — 自动接受文件编辑”

项目目录内的文件编辑自动批准。bash 命令除非有规则覆盖,仍需逐条审批。该模式被 auto mode 分类器用作”快速通道检查”——如果某个 tool 调用在 acceptEdits 下会被允许,则跳过昂贵的分类器 API 调用。

3. bypassPermissions — 完全信任模式

Section titled “3. bypassPermissions — 完全信任模式”

所有 tool 调用自动批准,以下情况除外

  • 显式 deny rule(管道中第 1a/1d 步)
  • 带有内容特定 pattern 的显式 ask rule(第 1f 步)
  • 对敏感路径(如 .git/.claude/、shell 配置文件)的安全检查(第 1g 步)
// src/utils/permissions/permissions.ts (line ~1268)
const shouldBypassPermissions =
appState.toolPermissionContext.mode === 'bypassPermissions' ||
(appState.toolPermissionContext.mode === 'plan' &&
appState.toolPermissionContext.isBypassPermissionsModeAvailable)

模型可以读取文件并规划行动,但不能执行写操作。当用户最初以 bypass 模式启动时,plan 模式通过 isBypassPermissionsModeAvailable 记录此信息,并可切换回去。

5. dontAsk — 对 permission prompt 自动拒绝

Section titled “5. dontAsk — 对 permission prompt 自动拒绝”

任何正常情况下会触发 permission prompt 的 tool 调用都将被自动拒绝。模型收到拒绝消息后必须寻找替代方案。

// src/utils/permissions/permissions.ts (line ~508)
if (appState.toolPermissionContext.mode === 'dontAsk') {
return {
behavior: 'deny',
decisionReason: { type: 'mode', mode: 'dontAsk' },
message: DONT_ASK_REJECT_MESSAGE(tool.name),
}
}

6. auto — AI 分类器模式(内部)

Section titled “6. auto — AI 分类器模式(内部)”

AI 分类器根据对话记录评估每个 tool 调用。分类器可自动批准安全操作或自动拒绝危险操作。当分类器不可用或达到拒绝限制时,回退至用户 prompt。

Mode文件读取文件写入安全 bash危险 bashSub-agents
default✅ 自动❓ 询问✅ 自动(只读)❓ 询问❓ 询问
acceptEdits✅ 自动✅ 自动(项目内)✅ 自动(只读)❓ 询问❓ 询问
bypassPermissions✅ 自动✅ 自动✅ 自动✅ 自动*✅ 自动*
plan✅ 自动❌ 拒绝❌ 拒绝❌ 拒绝❌ 拒绝
dontAsk✅ 自动❌ 拒绝✅ 自动(只读)❌ 拒绝❌ 拒绝
auto✅ 自动🤖 分类器🤖 分类器🤖 分类器🤖 分类器

* 对 .git/.claude/、shell 配置文件的安全检查始终会触发 prompt

每次 permission 检查返回以下三种核心 behavior 之一:

src/types/permissions.ts
export type PermissionBehavior = 'allow' | 'deny' | 'ask'

内部还使用第四种 behavior passthrough,表示”无规则匹配,继续下一项检查”:

export type PermissionResult<Input> =
| PermissionDecision<Input>
| {
behavior: 'passthrough'
message: string
suggestions?: PermissionUpdate[]
}

主入口是 permissions.ts 中的 hasPermissionsToUseTool,它委托给 hasPermissionsToUseToolInner,后者实现了一个有编号的步骤管道:

flowchart TD
    Start[Tool Invocation] --> Step1a

    subgraph "Phase 1: Rule-Based Checks"
        Step1a{1a. Entire tool<br/>denied by rule?}
        Step1a -->|Yes| Deny1[DENY]
        Step1a -->|No| Step1b

        Step1b{1b. Entire tool<br/>has ask rule?}
        Step1b -->|Yes, no sandbox| Ask1[ASK]
        Step1b -->|No / sandbox| Step1c

        Step1c[1c. Tool.checkPermissions<br/>tool-specific checks]
        Step1c --> Step1d

        Step1d{1d. Tool impl<br/>denied?}
        Step1d -->|Yes| Deny2[DENY]
        Step1d -->|No| Step1e

        Step1e{1e. Requires user<br/>interaction?}
        Step1e -->|Yes + ask| Ask2[ASK]
        Step1e -->|No| Step1f

        Step1f{1f. Content-specific<br/>ask rule?}
        Step1f -->|Yes| Ask3[ASK]
        Step1f -->|No| Step1g

        Step1g{1g. Safety check<br/>sensitive path?}
        Step1g -->|Yes| Ask4[ASK]
        Step1g -->|No| Phase2
    end

    subgraph "Phase 2: Mode-Based Decisions"
        Phase2{2a. Bypass<br/>permissions?}
        Phase2 -->|Yes| Allow1[ALLOW]
        Phase2 -->|No| Step2b

        Step2b{2b. Entire tool<br/>always allowed?}
        Step2b -->|Yes| Allow2[ALLOW]
        Step2b -->|No| Step3
    end

    subgraph "Phase 3: Final Resolution"
        Step3[3. Convert passthrough<br/>to ask] --> PostProcess
        PostProcess{Mode post-processing}
        PostProcess -->|dontAsk| DenyFinal[DENY]
        PostProcess -->|auto| Classifier[AI Classifier]
        PostProcess -->|headless| DenyHeadless[DENY]
        PostProcess -->|default| AskFinal[ASK user]
    end

    Classifier -->|approve| AllowClassifier[ALLOW]
    Classifier -->|block| DenyClassifier[DENY]
    Classifier -->|unavailable| FallbackAsk[ASK / DENY]

阶段一:基于规则的检查(bypass 免疫)

Section titled “阶段一:基于规则的检查(bypass 免疫)”

这些检查无论什么 mode 都会运行——即使是 bypassPermissions 也无法跳过:

// src/utils/permissions/permissions.ts (line ~1169)
async function hasPermissionsToUseToolInner(tool, input, context) {
// 1a. 整个 tool 被拒绝
const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
if (denyRule) {
return { behavior: 'deny', ... }
}
// 1b. 整个 tool 应当始终询问
const askRule = getAskRuleForTool(appState.toolPermissionContext, tool)
if (askRule) { ... }
// 1c. Tool 特定权限检查
toolPermissionResult = await tool.checkPermissions(parsedInput, context)
// 1d. Tool 被拒绝
// 1e. 需要用户交互
// 1f. 内容特定 ask rule
// 1g. 安全检查(敏感路径)
}

规则检查通过后,mode 决定默认行为:

// Step 2a: Bypass mode
if (shouldBypassPermissions) {
return { behavior: 'allow', decisionReason: { type: 'mode', mode: ... } }
}
// Step 2b: Tool 级别 allow rule
const alwaysAllowedRule = toolAlwaysAllowedRule(context, tool)
if (alwaysAllowedRule) {
return { behavior: 'allow', decisionReason: { type: 'rule', rule: ... } }
}

外层 hasPermissionsToUseTool 包装器对 ask 结果应用 mode 特定转换:

  1. dontAsk mode → 将 ask 转换为 deny
  2. auto mode → 路由至 AI 分类器而非用户 prompt
  3. headless/async agent → 运行 PermissionRequest hook,若没有 hook 给出决策则自动拒绝

rule 是细粒度访问控制的核心机制。每条规则包含三个组件:

src/types/permissions.ts
export type PermissionRule = {
source: PermissionRuleSource // 规则来源
ruleBehavior: PermissionBehavior // allow、deny 或 ask
ruleValue: PermissionRuleValue // tool + 可选内容 pattern
}
export type PermissionRuleValue = {
toolName: string // 例如 "Bash"、"Edit"、"mcp__server__tool"
ruleContent?: string // 例如 "npm install:*"、"git commit"
}

rule 以字符串形式存储,由 permissionRuleParser.ts 解析:

Rule 字符串解析结果含义
Bash{ toolName: "Bash" }整个 Bash tool
Bash(npm install){ toolName: "Bash", ruleContent: "npm install" }精确命令
Bash(git:*){ toolName: "Bash", ruleContent: "git:*" }前缀 pattern
Bash(*){ toolName: "Bash" }Bash(通配符 = 整个 tool)
Edit{ toolName: "Edit" }整个 Edit tool
mcp__server1{ toolName: "mcp__server1" }来自 MCP server 的所有 tool

rule 来自 7 个不同来源,各自具有不同的持久性和优先级:

src/types/permissions.ts
export type PermissionRuleSource =
| 'userSettings' // ~/.claude/settings.json
| 'projectSettings' // .claude/settings.json(共享,已提交)
| 'localSettings' // .claude/settings.local.json(gitignored)
| 'flagSettings' // --settings CLI flag 文件
| 'policySettings' // 受管理的企业 settings
| 'cliArg' // --allowed-tools、--denied-tools CLI 参数
| 'command' // 会话中的 /allowed-tools 命令
| 'session' // 内存中的会话 rule(用户批准)

rule 按优先级顺序匹配:deny > ask > allow

对于每种 behavior,从所有来源收集规则并与 tool 调用匹配:

src/utils/permissions/permissions.ts
export function getAllowRules(context: ToolPermissionContext): PermissionRule[] {
return PERMISSION_RULE_SOURCES.flatMap(source =>
(context.alwaysAllowRules[source] || []).map(ruleString => ({
source,
ruleBehavior: 'allow',
ruleValue: permissionRuleValueFromString(ruleString),
})),
)
}

用户批准某次 tool 使用时,rule 保存至 session 来源,直到 Claude Code 退出为止。

本地 Settings(项目专属,私有)

Section titled “本地 Settings(项目专属,私有)”

.claude/settings.local.json 已被 gitignore。保存在此处的 rule 跨会话持久,但仅保留在开发者本机。

.claude/settings.json 提交到代码仓库,此处的 rule 适用于所有团队成员。

~/.claude/settings.json 适用于该用户的所有项目。

managed-settings.json 或远程 API settings。当 allowManagedPermissionRulesOnly 启用时,只有 policy rule 生效——其他所有来源均被清除:

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

所有 permission 状态被打包到一个单一的不可变 context 对象中,贯穿整个管道流转:

src/types/permissions.ts
export type ToolPermissionContext = {
readonly mode: PermissionMode
readonly additionalWorkingDirectories: ReadonlyMap<string, AdditionalWorkingDirectory>
readonly alwaysAllowRules: ToolPermissionRulesBySource
readonly alwaysDenyRules: ToolPermissionRulesBySource
readonly alwaysAskRules: ToolPermissionRulesBySource
readonly isBypassPermissionsModeAvailable: boolean
readonly strippedDangerousRules?: ToolPermissionRulesBySource
readonly shouldAvoidPermissionPrompts?: boolean
readonly awaitAutomatedChecksBeforeDialog?: boolean
readonly prePlanMode?: PermissionMode
}

关键字段:

  • alwaysAllowRules / alwaysDenyRules / alwaysAskRules:按来源分组的 rule,无需重新解析即可高效查找
  • shouldAvoidPermissionPrompts:为 true 时(headless/async agent),ask 决策转换为 deny
  • isBypassPermissionsModeAvailable:记录用户切换到 plan 模式前是否处于 bypass 模式
  • strippedDangerousRules:进入 auto mode 时被移除的 rule(危险 pattern,如 Bash(python:*)

每个决策都携带 decisionReason,解释为何授予或拒绝权限。这对于调试和 UI 展示有意义的信息至关重要:

export type PermissionDecisionReason =
| { type: 'rule'; rule: PermissionRule }
| { type: 'mode'; mode: PermissionMode }
| { type: 'subcommandResults'; reasons: Map<string, PermissionResult> }
| { type: 'hook'; hookName: string; reason?: string }
| { type: 'classifier'; classifier: string; reason: string }
| { type: 'safetyCheck'; reason: string; classifierApprovable: boolean }
| { type: 'sandboxOverride'; reason: 'excludedCommand' | 'dangerouslyDisableSandbox' }
| { type: 'workingDir'; reason: string }
| { type: 'asyncAgent'; reason: string }
| { type: 'other'; reason: string }

safetyCheck 原因尤为值得关注——它包含 classifierApprovable 标志,决定 auto mode 分类器是否允许覆盖该安全检查。敏感文件路径(.claude/.git/、shell 配置文件)的 classifierApprovabletrue,而 Windows 路径 bypass 尝试的 classifierApprovablefalse

auto mode 包含一个拒绝追踪系统,防止分类器拒绝的无限循环:

// Referenced in permissions.ts
const denialState = context.localDenialTracking ??
appState.denialTracking ??
createDenialTrackingState()
// 达到拒绝限制时,回退至用户 prompt
const denialLimitResult = handleDenialLimitExceeded(newDenialState, ...)

当连续拒绝次数超过限制时,系统回退至用户 prompt。在 headless 模式下,它会抛出 AbortError 以防止失控的 agent 循环。

permission model 遵循纵深防御原则:

  1. deny rule 最先检查,任何 mode 都无法绕过
  2. 安全检查 bypass 免疫——即使 bypassPermissions 也会遵守
  3. mode 决定未覆盖操作的默认行为
  4. rule 在 tool 和命令级别提供细粒度控制
  5. 多层持久化级别支持团队和个人偏好
  6. 决策原因构成每个 permission 决策的审计跟踪