Claude Code 的权限系统
这一章要解决的问题是:模型已经决定要调用工具了,Claude Code 凭什么判断这次工具调用能不能真的执行?
源码里的权限系统不是一个确认弹窗,也不是某个总开关。它是一条前置在副作用之前的判定管线:
- 权限模式决定默认姿态。
- 权限规则表达用户、项目、企业和会话的安全意图。
- 工具自己的
checkPermissions()补充语义检查。 - 路径和命令规则拦住文本绕过和 TOCTOU。
dontAsk、auto、bypassPermissions在最后改变ask的处理方式,但不是所有安全检查都能绕过。
核心直觉
Claude Code 的权限系统不是“模型请求后问不问用户”,而是“工具副作用发生前,系统如何把 allow / deny / ask 这个三态决策算出来”。
先看源码入口
| 源码定位 | 关键符号 | 负责什么 |
|---|---|---|
types/permissions.ts | ExternalPermissionMode、InternalPermissionMode、PermissionMode | 定义外部模式和内部模式 |
PermissionMode.ts | 模式配置对象 | 模式标题、符号、颜色和显示信息 |
getNextPermissionMode.ts | getNextPermissionMode()、canCycleToAuto() | Shift+Tab 模式切换顺序和 auto 可用性 |
permissionSetup.ts | transitionPermissionMode() | 模式切换副作用,比如 plan 准备、auto 危险规则剥离 |
types/permissions.ts | PermissionRule、PermissionRuleValue、PermissionUpdate | 权限规则和持久化更新的数据结构 |
permissionRuleParser.ts | permissionRuleValueFromString() | 解析 ToolName(content) 规则字符串 |
shellRuleMatching.ts | parsePermissionRule()、matchWildcardPattern() | Bash 规则的 exact / prefix / wildcard 匹配 |
permissions.ts | hasPermissionsToUseTool()、hasPermissionsToUseToolInner() | 权限管线核心入口 |
pathValidation.ts | validatePath()、isPathAllowed() | 文件路径验证、工作区判断、安全路径保护 |
permissionSetup.ts | isDangerousBashPermission()、stripDangerousPermissionsForAutoMode() | auto 模式进入时剥离危险 allow 规则 |
shadowedRuleDetection.ts | UnreachableRule | 检测被 deny / ask 遮蔽的无效 allow 规则 |
这张表里最重要的是 hasPermissionsToUseTool()。模式、规则、路径验证、分类器最后都要汇入它。
总流程图
这张图有一个关键顺序:deny 和安全检查在前,模式裁决在后。bypassPermissions 并不是把整条管线从头跳过,它在阶段二才起作用。
权限模式:粗粒度姿态,不是最终裁决
外部可见的权限模式定义在 types/permissions.ts。
export const EXTERNAL_PERMISSION_MODES = [
'acceptEdits',
'bypassPermissions',
'default',
'dontAsk',
'plan',
] as const
内部还有两个模式。
export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'
export type PermissionMode = InternalPermissionMode
这段代码证明:公开 API 和内部运行时不是完全同一组模式。auto 和 bubble 是内部扩展,auto 还受 feature('TRANSCRIPT_CLASSIFIER') 等门控影响。
| 模式 | 运行时含义 | 关键边界 |
|---|---|---|
default | 保守默认,大量操作进入确认 | 初始安全姿态 |
acceptEdits | 工作区内编辑更容易自动通过 | Shell 命令仍需规则和确认 |
plan | 偏只读和规划 | 不应执行写操作 |
bypassPermissions | 阶段二直接放行 | 阶段一的 deny / ask / 安全检查仍可先拦 |
dontAsk | 不弹确认,把 ask 变成 deny | 适合非交互运行 |
auto | 用分类器处理 ask 灰区 | 规则和硬安全检查仍在前面 |
bubble | 内部模式 | 作为内部控制态使用 |
模式切换有副作用
模式切换不是改枚举值。transitionPermissionMode() 会处理进入或离开某些模式时的状态迁移。
| 切换 | 触发动作 | 为什么 |
|---|---|---|
进入 plan | prepareContextForPlanMode(),保存 prePlanMode | 计划模式需要记住之前从哪里来 |
进入 auto | stripDangerousPermissionsForAutoMode() | 防止危险 allow 规则让分类器失去意义 |
离开 auto | restoreDangerousPermissions() | 恢复被临时剥离的规则 |
离开 plan | 标记 hasExitedPlanMode | 让后续流程知道已经脱离计划态 |
这说明权限模式本身也带状态管理,不是 UI 档位。
权限规则:三字段表达安全意图
权限规则的数据结构很小,但它承载了用户、项目、企业和会话级策略。
export type PermissionRule = {
source: PermissionRuleSource
ruleBehavior: PermissionBehavior
ruleValue: PermissionRuleValue
}
规则目标由工具名和可选内容组成。
export type PermissionRuleValue = {
toolName: string
ruleContent?: string
}
这段代码证明:权限规则不是只看工具名。Bash 可以是工具级规则,也可以是 Bash(git status)、Bash(npm:*) 这类内容级规则。
规则来源有优先级
原文列出八种规则来源:
| 来源 | 典型位置 | 影响范围 |
|---|---|---|
policySettings | 企业托管策略 | 组织统一下发 |
projectSettings | .claude/settings.json | 项目共享,可提交 git |
localSettings | .claude/settings.local.json | 本地私有,通常 gitignore |
userSettings | ~/.claude/settings.json | 用户全局 |
flagSettings | --settings | 运行时指定 |
cliArg | --allowed-tools 等 | 命令行临时规则 |
command | 命令上下文 | 当前命令范围 |
session | 会话内临时规则 | 当前会话 |
这说明权限不是只有一份配置文件。企业策略、项目约定、个人偏好和会话临时授权会合并进同一套裁决管线。
规则字符串解析
规则在配置中通常写成字符串:
Bash
Bash(git status)
Bash(npm:*)
Edit(/path/to/file)
permissionRuleValueFromString() 负责解析 ToolName(content),并处理内容里的括号转义。原文还指出 Bash() 和 Bash(*) 都会被视为工具级规则,等价于 Bash。
这个设计很实用,但也带来一个后果:Bash(*) 不是“只允许一个星号参数”,而是允许 Bash 工具本身。读规则时不能把括号里的 * 当成普通 shell glob。
命令规则匹配:exact、prefix、wildcard 三种语义
Shell 权限规则不能只靠字符串包含。shellRuleMatching.ts 把规则解析成判别联合。
export type ShellPermissionRule =
| { type: 'exact'; command: string }
| { type: 'prefix'; prefix: string }
| { type: 'wildcard'; pattern: string }
这段代码证明 Bash 规则有三类匹配语义:
| 类型 | 规则示例 | 匹配 | 不匹配 |
|---|---|---|---|
exact | git status | git status | git status --short |
prefix | npm:* | npm install、npm test | npx npm |
wildcard | docker build -t * | docker build -t app | docker run app |
前缀规则来自 legacy :* 语法:
const match = rule.match(/^(.+):\*$/)
通配符匹配要处理 \* 字面星号。源码用 null-byte 哨兵把转义星号临时替换掉,避免正则转换时把字面 * 当 wildcard。
const ESCAPED_STAR_PLACEHOLDER = '\x00ESCAPED_STAR\x00'
const ESCAPED_BACKSLASH_PLACEHOLDER = '\x00ESCAPED_BACKSLASH\x00'
这段代码证明,规则匹配不是简单 includes('*')。它要区分“通配符星号”和“用户想匹配字面星号”,否则权限规则本身会出现绕过或误拦。
权限管线:hasPermissionsToUseTool() 的三阶段
真正执行工具前,权限系统会走 hasPermissionsToUseTool()。原文把它拆成三阶段:规则验证、模式裁决、模式后处理。
阶段一:deny、ask 和工具自检先跑
阶段一是最防御性的部分。它检查工具级 deny、工具级 ask、工具自身 checkPermissions(),然后再检查内容级 ask 和安全检查。
// 简化后的行为结构
if (matchesToolLevelDenyRule(tool)) {
return deny
}
if (matchesToolLevelAskRule(tool)) {
return ask
}
const toolDecision = tool.checkPermissions(input, context)
if (toolDecision.behavior === 'deny') {
return deny
}
if (toolDecision.behavior === 'ask') {
return ask
}
if (matchesContentLevelAskRule(call)) {
return ask
}
if (matchesSafetyCheck(call)) {
return ask
}
这段伪源码证明:工具自己有第二道语义闸门。比如 Bash 要解析命令,FileEdit 要看路径和写入行为,PowerShell 要检查 PowerShell 特有风险。系统不会只用全局规则替代工具语义。
阶段一里有两个关键点:
- 内容级 ask 规则在
bypassPermissions下也必须提示。 .git/、.claude/、.vscode/、shell 配置文件等安全检查也具有 bypass 免疫性。
这说明 bypassPermissions 不覆盖用户显式表达的 ask 安全意图,也不覆盖系统认为必须确认的危险路径。
阶段二:模式和 allow 规则
阶段二才处理 bypassPermissions、allow 规则和工具自身 allow。
if (permissionMode === 'bypassPermissions') {
return allow
}
if (matchesAllowRule(call)) {
return allow
}
if (toolDecision.behavior === 'allow') {
return allow
}
return ask
这段伪源码证明:bypassPermissions 不是最前面的万能通行证。它只在阶段一没有返回 deny 或强制 ask 之后才直接放行。
阶段三:ask 的后处理
阶段三处理的是“已经变成 ask 的请求怎么办”。
| 模式 | 对 ask 的处理 |
|---|---|
dontAsk | 转成 deny,不弹确认 |
auto | 调用分类器裁决,安全则 allow,不安全或不确定则回到确认 |
| 其他模式 | 弹出权限确认对话框 |
这个设计很重要:auto 分类器不是替代阶段一和阶段二,它只接管 ask 灰区。硬 deny 和 bypass 免疫的安全检查仍然在前面。
自动模式:先剥离危险 allow 规则
当进入 auto 模式,系统会临时剥离危险 Bash allow 规则。核心判断是 isDangerousBashPermission()。
export function isDangerousBashPermission(
toolName: string,
ruleContent: string | undefined,
): boolean {
if (toolName !== BASH_TOOL_NAME) {
return false
}
if (ruleContent === undefined || ruleContent === '') {
return true
}
const content = ruleContent.trim().toLowerCase()
if (content === '*') {
return true
}
// ...检查 DANGEROUS_BASH_PATTERNS
}
这段代码证明:Bash 工具级 allow、空内容、独立 * 都会被认为危险。进入 auto 模式时,这些规则会被临时剥离,离开 auto 再恢复。
危险模式还包括:
| 规则形态 | 风险 |
|---|---|
Bash | 允许所有 shell 命令 |
Bash(*) | 等价于工具级 allow |
Bash(python:*) | 允许任意 Python 代码 |
Bash(node *) | 允许任意 Node 命令 |
Bash(python -*) | 可能允许 python -c 这类任意代码执行 |
DANGEROUS_BASH_PATTERNS 包含多类解释器、shell、远程执行和包运行器,例如 python、node、ruby、perl、npx、bunx、npm run、bash、sh、ssh 等。内部构建还会额外检测 gh、curl、wget、git、kubectl、aws 等。
容易误解
auto 模式不是“有 allow 规则就照单全收”。进入 auto 前先剥离危险 allow,是为了避免分类器被一个过宽的历史授权绕过。
路径验证:拒绝语义不确定的路径
文件工具的风险往往不在“读写”两个字,而在路径。validatePath() 是路径权限验证入口。
共享路径防护:防止 UNC 泄漏凭据
Windows UNC 路径如 \\server\share 会触发系统自动发送 NTLM 凭据。攻击者可以诱导模型读取恶意 UNC 路径,从而泄漏认证哈希。
源码检测三类变体:
const backslashUncPattern = /\\\\[^\s\\/]+(?:@(?:\d+|ssl))?(?:[\\/]|$|\s)/i
const forwardSlashUncPattern = /(?<!:)\/\/[^\s\\/]+(?:@(?:\d+|ssl))?(?:[\\/]|$|\s)/i
这段代码证明:正斜杠 UNC 要用 (?<!:) 排除 URL 的 https://。主机名使用排除集而不是白名单,是为了捕获 Unicode 同形异义字这类欺骗。
时序一致性:TOCTOU 防护
路径验证最核心的判断是:验证时和执行时的语义必须一致。
| 输入形态 | 风险 |
|---|---|
~root/... | 验证时可能像相对路径,shell 执行时展开到 root |
$HOME/.ssh/id_rsa | 验证时是字面字符串,执行时展开为真实敏感路径 |
%USERPROFILE% | Windows 环境变量展开 |
=rg | Zsh equals 展开为可执行文件路径 |
| 写操作中的 glob | 验证的是模式,执行时可能匹配多个目标 |
源码的策略不是尝试完整模拟每种 shell 展开,而是直接拒绝这类语义不确定输入。这是 fail-closed:不确定就不要把它当安全路径。
isPathAllowed() 的顺序
路径规范化后,isPathAllowed() 做最终裁决。
| 顺序 | 检查 | 结果 |
|---|---|---|
| 1 | deny 规则 | 立即拒绝 |
| 2 | 内部可编辑路径 | plan、scratchpad、agent memory 等自动允许 |
| 3 | 危险目录和文件 | .git/、.claude/、shell rc 文件等需要确认 |
| 4 | 工作目录 | read 自动通过,write 依赖 acceptEdits 等模式 |
| 5 | sandbox 写白名单 | sandbox 允许写的目录放行 |
| 6 | allow 规则 | 匹配后放行 |
这说明路径规则也是多层的:不是“在 cwd 内就安全”,也不是“allow 规则永远最后说了算”。危险路径的 safety check 可以先把请求拉回确认。
危险文件和目录:bypass 也绕不过
源码把高风险对象列成常量。
export const DANGEROUS_FILES = [
'.gitconfig', '.gitmodules',
'.bashrc', '.bash_profile', '.zshrc', '.zprofile', '.profile',
'.ripgreprc', '.mcp.json', '.claude.json',
] as const
export const DANGEROUS_DIRECTORIES = [
'.git', '.vscode', '.idea', '.claude',
] as const
这段代码证明:危险不是只看“是否在项目内”。.gitconfig 能配置 core.sshCommand,shell rc 文件会在 shell 启动时执行,.vscode/settings.json 可以影响任务和终端行为,.mcp.json 会影响外部工具连接。
这些写操作会被 checkPathSafetyForAutoEdit() 标记为 safetyCheck,具有 bypass 免疫性。部分安全检查在 auto 模式下可能标记 classifierApprovable: true,但不是所有跨边界风险都能交给分类器。
危险删除路径也有专门检测。isDangerousRemovalPath() 防止删除根目录、主目录、Windows 驱动器根目录及其直接子目录,比如 /usr、/tmp、C:\Windows。
被遮蔽规则:配置了也可能永远不会生效
权限规则存在来源和优先级,就会出现遮蔽。比如高优先级规则 deny 了 Bash,低优先级规则 allow 了 Bash(git:*),后者永远到不了。
shadowedRuleDetection.ts 用 UnreachableRule 表达这种情况。
export type UnreachableRule = {
rule: PermissionRule
reason: string
shadowedBy: PermissionRule
shadowType: ShadowType
fix: string
}
这段代码证明:Claude Code 不只是执行规则,还会诊断规则配置是否自相矛盾。权限系统需要可解释,否则用户会以为“我已经 allow 了,为什么还是不行”。
权限更新:确认对话会写回规则
权限对话框里的“始终允许”不是只改内存状态。源码用 PermissionUpdate 描述可持久化的权限修改。
| 操作 | 含义 |
|---|---|
addRules | 增加 allow / deny / ask 规则 |
replaceRules | 替换某组规则 |
removeRules | 删除规则 |
setMode | 修改权限模式 |
addDirectories | 增加允许目录 |
removeDirectories | 移除允许目录 |
每个更新还会指定目标存储,比如 localSettings。这就是为什么一次确认可以变成后续会话的本地规则,而不是只能影响当前工具调用。
状态和数据结构
| 结构 | 关键字段 | 影响 |
|---|---|---|
PermissionMode | default、acceptEdits、plan、bypassPermissions、dontAsk、auto、bubble | 决定默认姿态和 ask 后处理 |
PermissionRule | source、ruleBehavior、ruleValue | 表达不同来源的 allow / deny / ask 规则 |
PermissionRuleValue | toolName、ruleContent | 区分工具级和内容级规则 |
ShellPermissionRule | exact、prefix、wildcard | Bash 命令规则匹配语义 |
PermissionUpdate | addRules 等六类操作 | 把确认结果或配置变更持久化 |
UnreachableRule | rule、shadowedBy、shadowType、fix | 诊断被遮蔽的无效规则 |
strippedDangerousRules | 危险 allow 规则列表 | auto 模式临时剥离,退出后恢复 |
设计取舍
| 取舍 | 源码证据 | 工程判断 |
|---|---|---|
| 三态而不是二态 | PermissionBehavior: allow / deny / ask | 不确定时交给用户,不冒充安全 |
| deny 和安全检查前置 | hasPermissionsToUseToolInner() 阶段一 | 高风险在模式裁决前先拦 |
bypassPermissions 不绕过所有检查 | 内容级 ask 和 safety check bypass 免疫 | 批量效率不能覆盖用户显式安全意图 |
工具有自己的 checkPermissions() | 阶段一调用工具自检 | Bash、Edit、PowerShell 的风险语义不同 |
| Bash 规则分 exact / prefix / wildcard | ShellPermissionRule 联合类型 | 权限规则要表达命令级粒度 |
| auto 模式剥离危险 allow | isDangerousBashPermission() | 防止过宽历史授权绕过分类器 |
| 路径验证拒绝 shell 展开 | validatePath() 对 $、%、= 等保守拒绝 | 避免验证时和执行时语义不一致 |
| 危险路径单独列常量 | DANGEROUS_FILES、DANGEROUS_DIRECTORIES | 工作区内也可能有代码执行入口 |
| 遮蔽规则可诊断 | UnreachableRule | 权限系统要能解释配置为何不生效 |
读源码抓手
- 先读
types/permissions.ts,把PermissionMode、PermissionRule、PermissionUpdate三个类型弄清楚。 - 再看
getNextPermissionMode()和transitionPermissionMode(),理解模式切换不是纯 UI。 - 接着读
permissionRuleValueFromString(),确认配置里的ToolName(content)怎么进结构化规则。 - 然后读
shellRuleMatching.ts,重点看:*、*、\*的不同语义。 - 核心部分读
hasPermissionsToUseTool(),按阶段一、阶段二、阶段三画出分支。 - 之后看
pathValidation.ts,尤其是 UNC、tilde、shell 展开、glob 写操作这些 fail-closed 分支。 - 最后读
isDangerousBashPermission()和stripDangerousPermissionsForAutoMode(),理解 auto 模式为什么要先清理过宽 allow。
小结
小结
Claude Code 的权限系统是一条副作用前置的安全管线。
- 权限模式只决定默认姿态,真正裁决来自规则、工具自检、路径验证和模式后处理的组合。
allow / deny / ask三态让系统可以表达“可执行”“不可执行”和“不确定,需要人确认”。- Bash 权限规则支持 exact、prefix、wildcard 三种匹配,避免只按工具名粗暴放行。
bypassPermissions在阶段二才放行,无法覆盖前置 deny、内容级 ask 和危险路径安全检查。- 路径验证优先拒绝语义不确定的输入,防止 shell 展开、UNC、glob 写操作造成验证和执行不一致。
- auto 模式不是全自动裸奔,它进入前会剥离危险 allow,把灰区交给分类器而不是让历史授权绕过安全边界。