第十三篇:Permission Model 深度解析 —— Claude Code 如何让 AI 安全执行命令
第十三篇:Permission Model 深度解析 —— Claude Code 如何让 AI 安全执行命令
📚 系列文章 第13篇/共100篇 · 2026年7月 · ⏱️ 阅读时间约 14 分钟
源码位置:src/utils/permissions/*、src/types/permissions.ts
一、引言:为什么需要 Permission Model?
Claude Code 不是一个只会聊天的机器人——它会真的在你的机器上跑命令、读写文件、发网络请求。这意味着一个核心问题:
AI 想做的事,哪些可以直接做?哪些必须问你?哪些永远不允许?
Permission Model(权限模型)就是 Claude Code 用来回答这个问题的整套机制。它决定了:
- 🟢 allow:直接放行,不打扰你
- 🔴 deny:直接拒绝,AI 碰都碰不到
- 🟡 ask:弹出权限请求,等你点头
它是 Claude Code 能在"自主能力"和"安全可控"之间取得平衡的关键。没有它,AI 要么寸步难行(什么都问),要么危险至极(什么都干)。
本文从**模式(Mode)→ 规则(Rule)→ 判定(Check)→ 持久化(Update)→ 企业管控(Policy)**五个层次,拆解这套权限系统。
二、整体架构概览
┌──────────────────────────────────────────────────────────────┐
│ Permission Model │
├──────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Permission │ │ Permission │ │ Permission Check │ │
│ │ Mode │ │ Rule │ │ (canUseTool) │ │
│ │ 自主程度 │ │ 规则匹配 │ │ 核心判定引擎 │ │
│ └─────────────┘ └─────────────┘ └──────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Update │ │ Denial Track│ │ Policy / Managed │ │
│ │ 记住选择 │ │ 拒绝兜底 │ │ 企业管控 │ │
│ └─────────────┘ └─────────────┘ └──────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│
▼
Tool 执行 or 权限请求弹窗
数据流:
AI 请求调用 Tool(input)
│
▼
canUseTool(tool, input, ctx)
│
├── Mode 短路(bypass / dontAsk / plan)
├── allow 规则命中 ──► allow(直接执行)
├── deny 规则命中 ──► deny(拒绝)
├── hook 前置拦截 ──► deny / ask
├── classifier 判定 ──► allow / deny / ask
└── 都不命中 ──► ask(弹窗等你确认)
│
▼
用户选择(本次/始终允许/拒绝)
│
▼
applyPermissionUpdate → persist(写入 settings)
三、Permission Mode:AI 的"自主程度"
权限模型最外层是模式(Mode)——它定义了 AI 整体有多"自由"。源码在 src/utils/permissions/PermissionMode.ts 和 src/types/permissions.ts。
3.1 模式定义
// 外部可见的模式(settings.json / --permission-mode 可用)
export const EXTERNAL_PERMISSION_MODES = [
'acceptEdits', // 自动接受文件编辑
'bypassPermissions',// 跳过所有权限检查
'default', // 默认:按规则 + 询问
'dontAsk', // 不问(危险:直接执行,无确认)
'plan', // 计划模式:只思考不执行
] as const
// 内部模式额外包含 ant 专属的 auto / bubble
export type InternalPermissionMode =
| ExternalPermissionMode
| 'auto'
| 'bubble'
每个模式有展示配置:
const PERMISSION_MODE_CONFIG = {
default: { title: 'Default', external: 'default' },
plan: { title: 'Plan Mode', external: 'plan', symbol: PAUSE_ICON },
acceptEdits: { title: 'Accept edits', external: 'acceptEdits' },
bypassPermissions: { title: 'Bypass Permissions', external: 'bypassPermissions' },
dontAsk: { title: "Don't Ask", external: 'dontAsk' },
// auto: ant-only 的"自动模式",通过 TRANSCRIPT_CLASSIFIER feature gate 开启
}
3.2 模式怎么切换?
终端里按 Shift+Tab 循环切换模式,逻辑在 getNextPermissionMode():
// 默认循环:default → acceptEdits → plan → bypassPermissions → (auto?) → default
export function getNextPermissionMode(ctx, _team?): PermissionMode {
switch (ctx.mode) {
case 'default':
return 'acceptEdits' // 先开放编辑
case 'acceptEdits':
return 'plan' // 再收紧到只读计划
case 'plan':
return ctx.isBypassPermissionsModeAvailable
? 'bypassPermissions' // 再放开到全放行
: (canCycleToAuto(ctx) ? 'auto' : 'default')
// ...
}
}
💡 实战价值:
bypassPermissions和dontAsk是"双刃剑"——CI / 脚本场景极方便,但本地日常使用务必谨慎,因为它们会绕过所有确认。
四、Permission Rule:规则长什么样
模式是"全局开关",规则才是"精细控制"。规则的核心类型在 src/types/permissions.ts:
export type PermissionBehavior = 'allow' | 'deny' | 'ask'
// 一条规则 = 来源 + 行为 + 内容
export type PermissionRule = {
source: PermissionRuleSource // 这条规则从哪来
ruleBehavior: PermissionBehavior
ruleValue: PermissionRuleValue
}
export type PermissionRuleValue = {
toolName: string // 作用于哪个工具,如 "Bash" / "Edit" / "WebFetch"
ruleContent?: string // 可选的内容匹配,如 "npm install" / "src/**"
}
4.1 规则来源与优先级
规则可以从多个地方来,源码用 PERMISSION_RULE_SOURCES 定义了叠加顺序:
const PERMISSION_RULE_SOURCES = [
...SETTING_SOURCES, // userSettings → projectSettings → localSettings → policySettings
'cliArg', // 命令行 --allowedTools
'command', // 本次会话命令注入
'session', // 会话内临时规则
] as const
export type PermissionRuleSource =
| 'userSettings' | 'projectSettings' | 'localSettings'
| 'flagSettings' | 'policySettings'
| 'cliArg' | 'command' | 'session'
配置文件里这样写(settings.json):
{
"permissions": {
"allow": ["Read", "Edit", "Bash(git status)", "Bash(npm test)"],
"deny": ["Bash(rm -rf *)", "WebFetch"],
"ask": ["Bash(curl *)"]
}
}
4.2 规则字符串解析:ToolName(content)
规则在磁盘上存成字符串格式 "ToolName" 或 "ToolName(content)",解析在 permissionRuleParser.ts:
permissionRuleValueFromString('Bash') // => { toolName: 'Bash' }
permissionRuleValueFromString('Bash(npm install)') // => { toolName: 'Bash', ruleContent: 'npm install' }
内容里可能有括号(如命令),需要转义:
// 转义顺序很重要:先转义反斜杠,再转义括号
escapeRuleContent('psycopg2.connect()') // => 'psycopg2.connect\\(\\)'
unescapeRuleContent('psycopg2.connect\\(\\)') // => 'psycopg2.connect()'
工具改名也能兼容(旧名自动映射到新名):
const LEGACY_TOOL_NAME_ALIASES = {
Task: AGENT_TOOL_NAME, // Task → Agent
KillShell: TASK_STOP_TOOL_NAME,
AgentOutputTool: TASK_OUTPUT_TOOL_NAME,
BashOutputTool: TASK_OUTPUT_TOOL_NAME,
}
normalizeLegacyToolName('Task') // => 'Agent'
五、核心判定:canUseTool 如何决策
真正"拍板"的地方是 src/utils/permissions/permissions.ts 里的判定逻辑(概念模型):
function canUseTool(tool, input, ctx): PermissionDecision {
// 1) 模式短路
if (ctx.mode === 'bypassPermissions') return allow(input) // 全放行
if (ctx.mode === 'plan' && tool.mutates) return deny('计划模式禁止修改')
// 2) 收集并匹配 allow 规则
const allowRules = getAllowRules(ctx) // 跨所有 source 摊平
for (const rule of matchRules(tool, input, allowRules)) {
return allow(input)
}
// 3) deny 规则命中 → 直接拒绝
if (matchDenyRules(tool, input, ctx)) return deny('规则拒绝')
// 4) hook 前置拦截(PreToolUse hook 可 deny / ask)
// 5) classifier 判定(BashClassifier / YOLO 分类器)
// 6) 以上都不命中 → 询问用户
return ask(buildReason(tool, ctx))
}
5.1 决策来源(decisionReason)
当弹出权限请求时,Claude Code 会告诉用户为什么需要确认,来源有三类:
type PermissionDecisionReason =
| { type: 'rule'; rule: PermissionRule } // 被某条规则命中
| { type: 'hook'; hookName: string; reason?: string } // 被 hook 拦截
| { type: 'classifier'; classifier: string; reason: string } // 被分类器判定
拼成用户能看懂的提示语:
export function createPermissionRequestMessage(toolName, reason?) {
if (reason?.type === 'hook') {
return `Hook '${reason.hookName}' 要求确认此 ${toolName} 操作`
}
if (reason?.type === 'rule') {
return `根据规则 ${permissionRuleValueToString(reason.rule.ruleValue)} 需要确认`
}
// classifier ...
}
六、决策结果:allow / deny / ask
判定引擎返回统一的 PermissionDecision:
export type PermissionDecision =
| PermissionAllowDecision
| PermissionDenyDecision
| PermissionAskDecision
export type PermissionAllowDecision = {
behavior: 'allow'
updatedInput: Record<string, unknown> // 可能改写入参
updatedPermissions?: PermissionUpdate[] // 附带要记录的权限变更
}
export type PermissionDenyDecision = {
behavior: 'deny'
message: string
interrupt?: boolean
}
七、记住这次选择:权限更新与持久化
用户点"始终允许"时,Claude Code 不是只管这一次,而是把规则写回 settings。这是 PermissionUpdate 机制:
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 }
export type PermissionUpdateDestination =
| 'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg'
执行时:
applyPermissionUpdate(update, ctx) // 内存生效(本次会话)
persistPermissionUpdates([update], ctx) // 落盘(下次启动也有效)
持久化读取用了"宽松解析",避免因无关字段校验失败而丢失已有规则:
// permissionsLoader.ts:只解析 JSON、不强制校验,仅用于追加规则
function getSettingsForSourceLenient_FOR_EDITING_ONLY_NOT_FOR_READING(source) {
const content = readFileSync(getSettingsFilePathForSource(source))
return safeParseJSON(content, false) // 保留所有现有配置
}
7.1 用户分类:临时 vs 永久
在 SDK / MCP 权限提示里,用户选择会带上分类:
decisionClassification: 'user_temporary' // 仅本次允许
| 'user_permanent' // 永久记住(写入 settings)
| 'user_reject' // 永久拒绝
八、拒绝追踪与兜底
连续拒绝可能意味着规则配置有问题。Claude Code 用 denialTracking.ts 做"拒绝计数",超过阈值就自动降级为询问而非硬拒,避免把 AI 彻底卡死:
const DENIAL_LIMITS = { /* 按工具/会话维度的拒绝上限 */ }
recordDenial(tool) // 记一次拒绝
if (shouldFallbackToPrompting(state)) { // 超阈值
// 不再硬拒,改为继续询问用户
}
九、SDK / MCP 权限提示协议
当 Claude Code 作为库(SDK)或接入外部 MCP 服务器时,权限请求通过工具协议传递,定义在 PermissionPromptToolResultSchema.ts:
// 宿主收到的请求
inputSchema = z.object({
tool_name: z.string(), // 请求权限的工具名
input: z.record(z.string(), z.unknown()),// 工具入参
tool_use_id: z.string().optional(), // 请求 ID
})
// 宿主返回的决策
outputSchema = z.union([
z.object({
behavior: z.literal('allow'),
updatedInput: z.record(z.string(), z.unknown()),
updatedPermissions: z.array(permissionUpdateSchema()).optional(),
decisionClassification: z.enum(['user_temporary','user_permanent','user_reject']).optional(),
}),
z.object({
behavior: z.literal('deny'),
message: z.string(),
interrupt: z.boolean().optional(),
}),
])
💡 设计哲学: 把"权限决策"抽象成工具调用,使 SDK 宿主、MCP 服务器都能插入自己的审批逻辑——这是 Claude Code 能嵌入 VS Code、CI、Agent 平台的基础。
十、企业管控:policySettings 与托管规则
企业环境需要"用户不能绕过的安全基线"。permissionsLoader.ts 提供:
// 仅允许"托管(policy)"规则生效,忽略用户本地 allow 规则
export function shouldAllowManagedPermissionRulesOnly(): boolean {
return getSettingsForSource('policySettings')?.allowManagedPermissionRulesOnly === true
}
// 开启后,权限弹窗里隐藏"始终允许"选项
export function shouldShowAlwaysAllowOptions(): boolean {
return !shouldAllowManagedPermissionRulesOnly()
}
这让管理员可以强制 deny 某些危险命令,且用户无法用"始终允许"绕过——安全策略真正兜底。
十一、源码亮点总结
| 特性 | 实现位置 | 价值 |
|---|---|---|
| 模式分级 | PermissionMode.ts |
default/plan/acceptEdits/bypass/dontAsk 灵活切换 |
| 规则三态 | types/permissions.ts |
allow/deny/ask 覆盖全部场景 |
| 多源叠加 | PERMISSION_RULE_SOURCES |
user/project/local/policy/cli/session 优先级清晰 |
| 字符串解析 | permissionRuleParser.ts |
Tool(content) + 括号转义 + 旧名兼容 |
| 判定引擎 | permissions.ts |
模式短路 → 规则 → hook → classifier → ask |
| 持久化 | PermissionUpdate.ts |
用户选择写回 settings,跨会话生效 |
| 拒绝兜底 | denialTracking.ts |
超阈值自动降级为询问 |
| 协议化 | PermissionPromptToolResultSchema.ts |
SDK/MCP 可插入自定义审批 |
| 企业管控 | permissionsLoader.ts |
allowManagedPermissionRulesOnly 锁定基线 |
十二、下一篇预告
第14篇我们将深入 上下文管理(Context Management)——200K token 窗口怎么被高效利用,Claude Code 如何在长对话里保留关键信息、丢弃噪声。敬请期待!
📚 Claude Code 源码解析系列
- 源码泄露事件深度解析
- 开发环境搭建指南
- CLI入口与命令系统
- AI对话引擎全解析
- 工具系统深度解析
- CLI命令行解析核心
- Handler处理器链
- QueryEngine查询引擎
- query.ts API对话层
- callModel.ts API调用层
- AnthropicClient API客户端
- MessageBuilder 消息构建器
- Permission Model 权限模型 ← 本文
- Context Management 上下文管理(待续)
💡 如果这篇文章对你有帮助,欢迎点赞、收藏、关注!
❓ 有问题欢迎在评论区讨论!
#ClaudeCode #源码分析 #PermissionModel #权限模型 #安全 #TypeScript #AI编程
更多推荐


所有评论(0)