第十三篇: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.tssrc/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')
    // ...
  }
}

💡 实战价值: bypassPermissionsdontAsk 是"双刃剑"——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 源码解析系列

  1. 源码泄露事件深度解析
  2. 开发环境搭建指南
  3. CLI入口与命令系统
  4. AI对话引擎全解析
  5. 工具系统深度解析
  6. CLI命令行解析核心
  7. Handler处理器链
  8. QueryEngine查询引擎
  9. query.ts API对话层
  10. callModel.ts API调用层
  11. AnthropicClient API客户端
  12. MessageBuilder 消息构建器
  13. Permission Model 权限模型 ← 本文
  14. Context Management 上下文管理(待续)

💡 如果这篇文章对你有帮助,欢迎点赞、收藏、关注!

有问题欢迎在评论区讨论!

#ClaudeCode #源码分析 #PermissionModel #权限模型 #安全 #TypeScript #AI编程

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐