第 15 课:权限基础 — 模式、规则与风险分级

3 阅读9分钟

模块五:权限体系 | 前置依赖:第 10 课 | 预计学习时间:70 分钟


学习目标

完成本课后,你将能够:

  1. 列举 Claude Code 的权限模式并解释每种模式的行为差异
  2. 说明三级风险评估(LOW/MEDIUM/HIGH)如何影响权限判定
  3. 描述权限规则的来源层级与合并策略
  4. 理解 PermissionContext 在不同运行环境中的 handler 分发机制
  5. 追踪一次工具调用从 useCanUseTool 到最终判定的完整链路

15.1 权限模式(Permission Modes)

Claude Code 的权限系统围绕"模式"构建。模式决定了工具调用时系统的默认行为。

外部模式(用户可见)

// types/permissions.ts
export const EXTERNAL_PERMISSION_MODES = [
  'acceptEdits',       // 自动接受文件编辑,其余仍需确认
  'bypassPermissions', // 跳过所有权限检查(危险!)
  'default',           // 默认模式 — 每次调用都询问用户
  'dontAsk',           // 不询问,直接拒绝需要权限的操作
  'plan',              // 规划模式 — 只允许读取操作
] as const

内部模式(含运行时专用)

export type InternalPermissionMode =
  | ExternalPermissionMode
  | 'auto'    // ML 分类器自动判定(feature-gated)
  | 'bubble'  // 权限请求"冒泡"到上层(Swarm worker 用)

auto 模式通过 feature gate TRANSCRIPT_CLASSIFIER 控制,只有在构建时启用该特性后才会出现在运行时验证集中:

export const INTERNAL_PERMISSION_MODES = [
  ...EXTERNAL_PERMISSION_MODES,
  ...(feature('TRANSCRIPT_CLASSIFIER')
    ? (['auto'] as const)
    : ([] as const)),
] as const

模式行为对照表

┌────────────────────┬──────────┬──────────┬──────────┐
│ 模式               │ 读操作   │ 写操作   │ 危险操作 │
├────────────────────┼──────────┼──────────┼──────────┤
│ default            │ allow    │ ask      │ ask      │
│ acceptEdits        │ allow    │ allow*   │ ask      │
│ plan               │ allow    │ deny     │ deny     │
│ dontAsk            │ allow    │ deny     │ deny     │
│ bypassPermissions  │ allow    │ allow    │ allow    │
│ auto               │ allow    │ 分类器   │ 分类器   │
└────────────────────┴──────────┴──────────┴──────────┘

* acceptEdits 对文件编辑自动允许,对 Bash 等仍需确认

15.2 三级风险评估

每个权限判定都可附带风险等级,用于向用户展示操作的危险程度:

// types/permissions.ts
export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH'

export type PermissionExplanation = {
  riskLevel: RiskLevel
  explanation: string  // 给用户看的一句话解释
  reasoning: string    // 详细推理过程
  risk: string         // 风险描述
}

风险等级语义

LOW    ─── 读取操作、安全的文件操作
           例:cat README.md, ls src/
           
MEDIUM ─── 写操作、可逆的修改
           例:编辑源码文件、创建新文件
           
HIGH   ─── 不可逆操作、系统级影响
           例:rm -rf, git push --force, 修改 .bashrc

风险等级通过 Permission Explainer(LLM 调用)动态计算,这部分我们将在第 16 课详细讨论。


15.3 权限行为三态

权限判定的结果是三选一的 PermissionBehavior

export type PermissionBehavior = 'allow' | 'deny' | 'ask'

加上内部使用的 passthrough,完整决策类型为:

export type PermissionResult<Input> =
  | PermissionDecision<Input>   // allow | ask | deny
  | {
      behavior: 'passthrough'   // "我不管,交给下一个检查"
      message: string
      suggestions?: PermissionUpdate[]
      pendingClassifierCheck?: PendingClassifierCheck
    }

决策数据结构

PermissionAllowDecision
├── behavior: 'allow'
├── updatedInput?: Input        ← 可修改工具输入
├── userModified?: boolean      ← 用户是否编辑了输入
├── decisionReason?             ← 为什么允许
├── acceptFeedback?: string     ← 用户附加的反馈
└── contentBlocks?              ← 附带的内容块(如图片)

PermissionAskDecision
├── behavior: 'ask'
├── message: string             ← 显示给用户的问题
├── suggestions?: PermissionUpdate[]  ← 可选的一键操作
├── pendingClassifierCheck?     ← 后台分类器检查
└── isBashSecurityCheckForMisparsing? ← 安全检查标记

PermissionDenyDecision
├── behavior: 'deny'
├── message: string             ← 拒绝原因
└── decisionReason              ← 必须提供原因

15.4 权限规则系统

规则来源(PermissionRuleSource)

权限规则从七个来源汇聚,优先级从高到低:

export type PermissionRuleSource =
  | 'policySettings'     // 组织策略(最高优先级)
  | 'flagSettings'       // CLI 参数
  | 'cliArg'             // 命令行 --allow / --deny
  | 'localSettings'      // .claude/settings.local.json
  | 'projectSettings'    // .claude/settings.json
  | 'userSettings'       // ~/.claude/settings.json
  | 'command'            // 命令内置规则
  | 'session'            // 当前会话临时规则

规则结构

export type PermissionRuleValue = {
  toolName: string       // 例: "Bash", "FileEdit", "mcp__server1"
  ruleContent?: string   // 例: "prefix:git *", "*.ts" — 可选的内容匹配
}

export type PermissionRule = {
  source: PermissionRuleSource
  ruleBehavior: PermissionBehavior  // allow | deny | ask
  ruleValue: PermissionRuleValue
}

规则匹配示例

规则 "Bash"               → 匹配所有 Bash 工具调用
规则 "Bash(prefix:git *)" → 匹配以 "git " 开头的 Bash 命令
规则 "FileEdit(*.ts)"     → 匹配编辑 .ts 文件
规则 "mcp__server1"       → 匹配该 MCP server 的所有工具
规则 "mcp__server1__*"    → 同上(通配符形式)

ToolPermissionContext — 运行时权限上下文

所有规则最终汇聚到 ToolPermissionContext,存储在 AppState 中:

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

规则按行为分三桶存储(allow / deny / ask),每桶再按来源分层:

alwaysAllowRules = {
  userSettings:    ["Bash(prefix:git *)"],
  projectSettings: ["FileRead"],
  session:         ["Bash(prefix:npm test)"]
}

alwaysDenyRules = {
  policySettings:  ["Bash(prefix:curl *)"],
  userSettings:    ["WebFetch"]
}

15.5 受保护文件与目录

Claude Code 维护一份敏感文件/目录清单,对这些路径的写操作需要额外审批:

// utils/permissions/filesystem.ts
export const DANGEROUS_FILES = [
  '.gitconfig',      // Git 配置 — 可执行任意命令
  '.gitmodules',     // Git 子模块 — 可指向恶意仓库
  '.bashrc',         // Shell 启动脚本 — 代码注入
  '.bash_profile',
  '.zshrc',
  '.zprofile',
  '.profile',
  '.ripgreprc',      // ripgrep 配置
  '.mcp.json',       // MCP 服务器配置
  '.claude.json',    // Claude Code 配置
] as const

export const DANGEROUS_DIRECTORIES = [
  '.git',            // Git 内部结构
  '.vscode',         // VS Code 配置
  '.idea',           // JetBrains 配置
  '.claude',         // Claude Code 配置目录
] as const

大小写标准化防护

macOS/Windows 的文件系统不区分大小写,攻击者可以用 .cLauDe/Settings.locaL.json 绕过检查:

export function normalizeCaseForComparison(path: string): string {
  return path.toLowerCase()  // 始终转为小写比较
}

15.6 useCanUseTool — 权限判定入口

hooks/useCanUseTool.tsx 是所有工具调用的权限入口。它是一个 React Hook,返回一个异步函数:

export type CanUseToolFn<Input> = (
  tool: ToolType,
  input: Input,
  toolUseContext: ToolUseContext,
  assistantMessage: AssistantMessage,
  toolUseID: string,
  forceDecision?: PermissionDecision<Input>
) => Promise<PermissionDecision<Input>>

完整判定流程

useCanUseTool(tool, input, context, message, toolUseID)
  │
  ├─ 1. 检查中止信号(abortController.signal)
  │     └─ 已中止? → cancelAndAbort()
  │
  ├─ 2. hasPermissionsToUseTool(tool, input, context)
  │     │  ← 核心权限检查(见下方)
  │     │
  │     ├─ result.behavior === 'allow'
  │     │  ├─ 记录分类器审批(如有)
  │     │  ├─ logDecision('accept', 'config')
  │     │  └─ resolve(buildAllow(...))
  │     │
  │     ├─ result.behavior === 'deny'
  │     │  ├─ logDecision('reject', 'config')
  │     │  ├─ auto 模式: 记录拒绝 + 通知 UI
  │     │  └─ resolve(result)
  │     │
  │     └─ result.behavior === 'ask'
  │        │
  │        ├─ 3. awaitAutomatedChecksBeforeDialog?
  │        │  └─ handleCoordinatorPermission()
  │        │     ├─ 运行 hooks
  │        │     ├─ 尝试分类器
  │        │     └─ 都不行 → 继续
  │        │
  │        ├─ 4. 是 Swarm worker?
  │        │  └─ handleSwarmWorkerPermission()
  │        │     ├─ 尝试分类器
  │        │     └─ 转发到 leader
  │        │
  │        ├─ 5. 投机分类器检查(2秒竞速)
  │        │  └─ peekSpeculativeClassifierCheck()
  │        │     ├─ 高置信度匹配 → 自动批准
  │        │     └─ 超时/不匹配 → 继续
  │        │
  │        └─ 6. handleInteractivePermission()
  │              └─ 向用户展示权限弹窗
  │
  └─ catch → 错误处理 → cancelAndAbort()

hasPermissionsToUseTool 内部逻辑

这个函数在 utils/permissions/permissions.ts 中实现,是真正的"规则引擎":

hasPermissionsToUseTool(tool, input, context)
  │
  ├─ hasPermissionsToUseToolInner()
  │  ├─ 检查 deny 规则 → 命中 → deny
  │  ├─ 检查 allow 规则 → 命中 → allow
  │  ├─ 检查 ask 规则 → 命中 → ask
  │  ├─ 检查模式默认行为
  │  └─ 工具自身 checkPermissions()
  │
  ├─ allow? → 重置连续拒绝计数
  │
  ├─ ask + dontAsk 模式? → 转为 deny
  │
  └─ ask + auto 模式? → 交给 ML 分类器
     ├─ 安全检查不可自动批准? → 仍然 ask/deny
     ├─ 检查拒绝追踪阈值
     └─ 调用 classifyYoloAction()

15.7 PermissionContext — 权限操作上下文

hooks/toolPermission/PermissionContext.ts 创建一个冻结的上下文对象,封装权限判定过程中需要的所有操作:

function createPermissionContext(
  tool, input, toolUseContext, assistantMessage,
  toolUseID, setToolPermissionContext, queueOps?
) {
  const ctx = {
    tool, input, toolUseContext, assistantMessage,
    messageId, toolUseID,
    
    // 日志
    logDecision(args, opts),
    logCancelled(),
    
    // 权限持久化
    async persistPermissions(updates): Promise<boolean>,
    
    // 中止处理
    resolveIfAborted(resolve): boolean,
    cancelAndAbort(feedback?, isAbort?, contentBlocks?),
    
    // 构建决策
    buildAllow(updatedInput, opts?): PermissionAllowDecision,
    buildDeny(message, reason): PermissionDenyDecision,
    
    // 用户/Hook 批准处理
    async handleUserAllow(input, updates, feedback?, ...),
    async handleHookAllow(input, updates, ...),
    
    // 分类器(feature-gated)
    async tryClassifier(pending, updatedInput),
    
    // Hook 执行
    async runHooks(mode, suggestions, input?, ...),
    
    // 确认队列操作
    pushToQueue(item),
    removeFromQueue(),
    updateQueueItem(patch),
  }
  return Object.freeze(ctx)  // 不可变
}

三种 Handler

根据运行环境,权限请求走不同的 handler:

┌─────────────────────┬────────────────────────────────────┐
│ Handler             │ 使用场景                           │
├─────────────────────┼────────────────────────────────────┤
│ interactiveHandler  │ 主 Agent(有终端交互的场景)       │
│                     │ 展示权限弹窗给用户                 │
│                     │ 同时异步运行 hooks + 分类器        │
│                     │ 用户交互与自动检查竞速             │
├─────────────────────┼────────────────────────────────────┤
│ coordinatorHandler  │ Coordinator worker                 │
│                     │ 先顺序执行 hooks → 分类器          │
│                     │ 都不行才 fallthrough 到交互弹窗    │
├─────────────────────┼────────────────────────────────────┤
│ swarmWorkerHandler  │ Swarm worker(子进程)             │
│                     │ 尝试分类器自动批准                 │
│                     │ 不行就转发到 leader 处理           │
└─────────────────────┴────────────────────────────────────┘

interactiveHandler 的竞速模型

                    ┌─── hooks 执行 ──────── 结果? ─── resolve
                    │
用户看到弹窗 ──────┼─── 分类器异步执行 ──── 结果? ─── resolve
                    │
                    └─── 用户点击 ─────────── 选择? ─── resolve
                    
                    ↕ 三者竞速,第一个完成的 resolve 生效
                    ↕ ResolveOnce 保证只 resolve 一次

createResolveOnce 是关键的竞态安全组件:

function createResolveOnce<T>(resolve: (value: T) => void): ResolveOnce<T> {
  let claimed = false
  let delivered = false
  return {
    resolve(value: T) {
      if (delivered) return
      delivered = true; claimed = true
      resolve(value)
    },
    isResolved() { return claimed },
    claim() {        // 原子性检查+标记
      if (claimed) return false
      claimed = true
      return true
    },
  }
}

15.8 权限决策原因(DecisionReason)

每个权限决策都附带"为什么"的解释:

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

safetyCheck 类型特别值得注意:

{
  type: 'safetyCheck',
  reason: string,
  classifierApprovable: boolean  // true → auto 模式可让分类器判定
                                  // false → 必须人工确认
}

classifierApprovable: true 用于敏感文件路径(.claude/, .git/, shell 配置),分类器有上下文可以做出合理判断。classifierApprovable: false 用于 Windows 路径绕过尝试和跨机器 bridge 消息,必须由人工确认。


15.9 权限更新与持久化

用户在权限弹窗中做出的选择可以持久化为规则:

export type PermissionUpdate =
  | { type: 'addRules';       destination; rules; behavior }
  | { type: 'replaceRules';   destination; rules; behavior }
  | { type: 'removeRules';    destination; rules; behavior }
  | { type: 'setMode';        destination; mode }
  | { type: 'addDirectories'; destination; directories }
  | { type: 'removeDirectories'; destination; directories }

持久化目标:

export type PermissionUpdateDestination =
  | 'userSettings'     // ~/.claude/settings.json
  | 'projectSettings'  // .claude/settings.json
  | 'localSettings'    // .claude/settings.local.json
  | 'session'          // 仅当前会话(不写磁盘)
  | 'cliArg'           // 命令行参数级别

15.10 端到端示例:用户执行 npm test

用户输入: "运行测试"
  │
  ▼
模型生成: tool_use(Bash, {command: "npm test"})
  │
  ▼
useCanUseTool(BashTool, {command: "npm test"}, ...)
  │
  ├─ hasPermissionsToUseTool()
  │  ├─ 检查 deny 规则 → 无匹配
  │  ├─ 检查 allow 规则 → 假设有 "Bash(prefix:npm test)"
  │  └─ 匹配! → return { behavior: 'allow', decisionReason: { type: 'rule', ... } }
  │
  ├─ behavior === 'allow'
  ├─ logDecision('accept', 'config')
  └─ resolve(buildAllow({command: "npm test"}))
  
  → 工具直接执行,用户无感知

如果没有匹配的 allow 规则且模式是 default

  ├─ hasPermissionsToUseTool()
  │  ├─ 检查 deny 规则 → 无匹配
  │  ├─ 检查 allow 规则 → 无匹配
  │  └─ 默认模式 → return { behavior: 'ask', message: "..." }
  │
  ├─ behavior === 'ask'
  ├─ handleInteractivePermission()
  │  ├─ 显示弹窗: "Allow Bash: npm test? [y/n/always]"
  │  ├─ 用户选 "always"
  │  ├─ persistPermissions([{type:'addRules', ...}])
  │  └─ resolve(buildAllow(...))
  │
  → 规则保存到 settings.json,下次自动允许

课后练习

练习 1:模式推演

分析以下场景在不同模式下的行为:

  • default 模式下执行 rm -rf node_modules
  • acceptEdits 模式下编辑 .bashrc
  • plan 模式下运行 cat package.json
  • auto 模式下执行 git push origin main

练习 2:规则优先级

给定以下规则配置,判断 Bash(prefix:curl https://api.example.com) 的权限结果:

{
  "policySettings": { "deny": ["Bash(prefix:curl *)"] },
  "userSettings": { "allow": ["Bash(prefix:curl https://api.example.com)"] },
  "session": { "allow": ["Bash"] }
}

练习 3:Handler 选择

阅读 useCanUseTool.tsx 源码,画出以下场景各自走哪个 handler 路径:

  • 普通 CLI 终端中执行工具
  • SDK headless 模式(shouldAvoidPermissionPrompts: true
  • Swarm worker 子进程中执行工具

练习 4:ResolveOnce 竞态

实现一个简化版的 ResolveOnce<T>,测试以下场景:

  • 分类器和用户同时返回结果
  • 用户中止(abort)后分类器返回结果
  • Hook 拒绝后用户点击允许

本课小结

要点内容
权限模式7 种模式:default/acceptEdits/plan/dontAsk/bypassPermissions/auto/bubble
行为三态allow(允许)、deny(拒绝)、ask(询问用户)
风险等级LOW/MEDIUM/HIGH,通过 Permission Explainer 动态评估
规则来源7 层来源,policy 最高优先级,session 最低
受保护文件.gitconfig/.bashrc/.claude 等敏感配置
Handler 分发interactive(主 Agent)、coordinator(协调器)、swarmWorker(子进程)
竞速安全ResolveOnce + claim() 保证并发场景下只 resolve 一次

下一课预告

第 16 课:高级权限 — 自动模式与安全防护 — 深入 auto 模式的 ML 分类器实现、YOLO 分类器的两阶段判定、Bash 命令的路径遍历防护、以及拒绝追踪的熔断机制。