第十三篇: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 可用)exportconstEXTERNAL_PERMISSION_MODES=['acceptEdits',// 自动接受文件编辑'bypassPermissions',// 跳过所有权限检查'default',// 默认:按规则 + 询问'dontAsk',// 不问(危险:直接执行,无确认)'plan',// 计划模式:只思考不执行]asconst// 内部模式额外包含 ant 专属的 auto / bubbleexporttypeInternalPermissionMode=|ExternalPermissionMode|'auto'|'bubble'每个模式有展示配置:
constPERMISSION_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?) → defaultexportfunctiongetNextPermissionMode(ctx,_team?):PermissionMode{switch(ctx.mode){case'default':return'acceptEdits'// 先开放编辑case'acceptEdits':return'plan'// 再收紧到只读计划case'plan':returnctx.isBypassPermissionsModeAvailable?'bypassPermissions'// 再放开到全放行:(canCycleToAuto(ctx)?'auto':'default')// ...}}💡 实战价值:
bypassPermissions和dontAsk是"双刃剑"——CI / 脚本场景极方便,但本地日常使用务必谨慎,因为它们会绕过所有确认。
四、Permission Rule:规则长什么样
模式是"全局开关",规则才是"精细控制"。规则的核心类型在src/types/permissions.ts:
exporttypePermissionBehavior='allow'|'deny'|'ask'// 一条规则 = 来源 + 行为 + 内容exporttypePermissionRule={source:PermissionRuleSource// 这条规则从哪来ruleBehavior:PermissionBehavior ruleValue:PermissionRuleValue}exporttypePermissionRuleValue={toolName:string// 作用于哪个工具,如 "Bash" / "Edit" / "WebFetch"ruleContent?:string// 可选的内容匹配,如 "npm install" / "src/**"}4.1 规则来源与优先级
规则可以从多个地方来,源码用PERMISSION_RULE_SOURCES定义了叠加顺序:
constPERMISSION_RULE_SOURCES=[...SETTING_SOURCES,// userSettings → projectSettings → localSettings → policySettings'cliArg',// 命令行 --allowedTools'command',// 本次会话命令注入'session',// 会话内临时规则]asconstexporttypePermissionRuleSource=|'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()'工具改名也能兼容(旧名自动映射到新名):
constLEGACY_TOOL_NAME_ALIASES={Task:AGENT_TOOL_NAME,// Task → AgentKillShell:TASK_STOP_TOOL_NAME,AgentOutputTool:TASK_OUTPUT_TOOL_NAME,BashOutputTool:TASK_OUTPUT_TOOL_NAME,}normalizeLegacyToolName('Task')// => 'Agent'五、核心判定:canUseTool 如何决策
真正"拍板"的地方是src/utils/permissions/permissions.ts里的判定逻辑(概念模型):
functioncanUseTool(tool,input,ctx):PermissionDecision{// 1) 模式短路if(ctx.mode==='bypassPermissions')returnallow(input)// 全放行if(ctx.mode==='plan'&&tool.mutates)returndeny('计划模式禁止修改')// 2) 收集并匹配 allow 规则constallowRules=getAllowRules(ctx)// 跨所有 source 摊平for(construleofmatchRules(tool,input,allowRules)){returnallow(input)}// 3) deny 规则命中 → 直接拒绝if(matchDenyRules(tool,input,ctx))returndeny('规则拒绝')// 4) hook 前置拦截(PreToolUse hook 可 deny / ask)// 5) classifier 判定(BashClassifier / YOLO 分类器)// 6) 以上都不命中 → 询问用户returnask(buildReason(tool,ctx))}5.1 决策来源(decisionReason)
当弹出权限请求时,Claude Code 会告诉用户为什么需要确认,来源有三类:
typePermissionDecisionReason=|{type:'rule';rule:PermissionRule}// 被某条规则命中|{type:'hook';hookName:string;reason?:string}// 被 hook 拦截|{type:'classifier';classifier:string;reason:string}// 被分类器判定拼成用户能看懂的提示语:
exportfunctioncreatePermissionRequestMessage(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:
exporttypePermissionDecision=|PermissionAllowDecision|PermissionDenyDecision|PermissionAskDecisionexporttypePermissionAllowDecision={behavior:'allow'updatedInput:Record<string,unknown>// 可能改写入参updatedPermissions?:PermissionUpdate[]// 附带要记录的权限变更}exporttypePermissionDenyDecision={behavior:'deny'message:stringinterrupt?:boolean}七、记住这次选择:权限更新与持久化
用户点"始终允许"时,Claude Code 不是只管这一次,而是把规则写回 settings。这是PermissionUpdate机制:
exporttypePermissionUpdate=|{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}exporttypePermissionUpdateDestination=|'userSettings'|'projectSettings'|'localSettings'|'session'|'cliArg'执行时:
applyPermissionUpdate(update,ctx)// 内存生效(本次会话)persistPermissionUpdates([update],ctx)// 落盘(下次启动也有效)持久化读取用了"宽松解析",避免因无关字段校验失败而丢失已有规则:
// permissionsLoader.ts:只解析 JSON、不强制校验,仅用于追加规则functiongetSettingsForSourceLenient_FOR_EDITING_ONLY_NOT_FOR_READING(source){constcontent=readFileSync(getSettingsFilePathForSource(source))returnsafeParseJSON(content,false)// 保留所有现有配置}7.1 用户分类:临时 vs 永久
在 SDK / MCP 权限提示里,用户选择会带上分类:
decisionClassification:'user_temporary'// 仅本次允许|'user_permanent'// 永久记住(写入 settings)|'user_reject'// 永久拒绝八、拒绝追踪与兜底
连续拒绝可能意味着规则配置有问题。Claude Code 用denialTracking.ts做"拒绝计数",超过阈值就自动降级为询问而非硬拒,避免把 AI 彻底卡死:
constDENIAL_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 规则exportfunctionshouldAllowManagedPermissionRulesOnly():boolean{returngetSettingsForSource('policySettings')?.allowManagedPermissionRulesOnly===true}// 开启后,权限弹窗里隐藏"始终允许"选项exportfunctionshouldShowAlwaysAllowOptions():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编程
