Claude Code 源码解析
返回目录
安全与权限10

权限系统

五层安全决策

核心洞察

YOLO 自动模式通过转录分类器实现无人值守下的安全执行

学习

概念讲解与核心设计分析

权限系统概述

Claude Code 的权限系统是安全性的最后一道防线。它决定了 Claude 能做什么、不能做什么、需要用户确认后才能做什么。这套系统由 5 层决策逻辑、多种权限模式、命令分类器、沙箱适配器和交互式确认对话框构成。每一次工具调用都要经过这套系统的审查。

5 层权限决策

当一个工具调用到达权限系统时,它按照以下 5 层顺序被评估,第一个做出决策的层"获胜":

第 1 层:全局拒绝规则(Blanket Deny Rules)

最高优先级。这些规则定义了无论如何都不允许的操作:

  • 例如:禁止访问 /etc/shadow、禁止执行 rm -rf /
  • 这些规则由系统硬编码,用户无法覆盖
  • 如果匹配,立即返回"拒绝",后续层不再评估

第 2 层:始终允许规则(Always-Allow Rules)

用户配置的自动允许规则(在 settings.json 中定义):

  • 例如:"always_allow": ["Read", "Glob", "Grep"] 自动允许所有只读工具
  • Bash 命令可以使用模式匹配:"Bash(npm test*)" 允许所有以 npm test 开头的命令
  • 如果匹配,立即返回"允许",跳过后续层

第 3 层:始终拒绝规则(Always-Deny Rules)

用户配置的自动拒绝规则:

  • 例如:"always_deny": ["Bash(curl*)", "Bash(wget*)"] 禁止网络下载命令
  • 优先级低于 always-allow,所以如果同一工具同时在两个列表中,always-allow 获胜

第 4 层:工具自身权限检查(Tool checkPermissions())

每个工具实现自己的 checkPermissions() 方法:

  • BashTool 会分析命令内容,判断是否需要用户确认
  • FileWriteTool 检查目标路径是否在工作目录内
  • 这一层可以返回"允许"、"拒绝"或"需要用户确认"

第 5 层:分类器 / 用户确认(Classifier / User Prompt)

如果前 4 层都没有做出明确决策,系统进入最终判断:

  • 在默认模式下,弹出交互式对话框让用户确认
  • 在自动模式(YOLO/Auto)下,由分类器自动判断
  • 用户确认对话框显示工具名称、输入参数和预期行为

PermissionMode:权限模式

系统支持 4 种权限模式,决定了第 5 层的行为:

  • default — 默认模式。未通过前 4 层的调用会弹出用户确认对话框
  • plan — 计划模式。只读工具自动允许,写操作工具直接拒绝
  • bypassPermissions — 绕过权限。所有工具自动允许(仅用于内部测试)
  • auto — 自动模式(YOLO 模式)。由分类器自动判断,不打扰用户

Bash 分类器(bashClassifier.ts)

BashTool 的权限判断比其他工具更复杂,因为 shell 命令的风险差异极大。bashClassifier.ts 专门负责 Bash 命令的安全分类:

  • 结合命令的 AST 分析结果(来自 Tree-sitter)
  • 判断命令是否涉及文件修改、网络访问、进程管理等高风险操作
  • 为每个命令生成安全评分和风险标签
  • 根据评分决定是否需要用户确认

YOLO/Auto 模式分类器(yoloClassifier.ts)

在自动模式下,yoloClassifier.ts 负责在不打扰用户的情况下做安全判断:

  • 分析整个对话上下文(transcript-level),而不只是单个工具调用
  • 考虑 Claude 为什么要执行这个操作(基于对话历史)
  • 对明显安全的操作(如用户刚要求的文件编辑)自动放行
  • 对可疑操作(如未预期的 rm 命令)仍然拒绝

拒绝追踪与回退提示(Denial Tracking)

当分类器多次拒绝 Claude 的工具调用时,系统会触发回退逻辑:

  • shouldFallbackToPrompting() 检测重复拒绝的模式
  • 如果同类操作被连续拒绝 3 次以上,回退到用户提示模式
  • 这避免了分类器过于保守导致 Claude 无法完成任务

沙箱适配器(sandbox-adapter.ts)

对于需要在隔离环境中执行的操作,sandbox-adapter.ts 提供了统一的沙箱接口:

  • 抽象不同平台的沙箱实现(macOS sandbox-exec、Linux namespaces 等)
  • 限制文件系统访问范围(只能访问工作目录和显式挂载的路径)
  • 限制网络访问
  • 限制进程能力(不允许提权操作)

useCanUseTool React Hook

useCanUseTool 是前端侧的权限集成点,连接后端权限系统和 UI 交互:

  • 当工具调用需要用户确认时,该 hook 触发一个交互式对话框
  • 对话框显示:工具名称、命令内容、预期行为、风险提示
  • 用户可以选择"允许"、"拒绝"、"始终允许此类操作"
  • 支持 coordinator/swarm 模式下的多 Agent 权限管理

Shell 规则匹配

Bash 命令的权限规则支持模式匹配(Pattern Matching):

  • Bash(git *) — 匹配所有以 git 开头的命令
  • Bash(npm run *) — 匹配所有 npm run 命令
  • Bash(cd *) — 匹配所有目录切换命令
  • 模式使用 glob 语法,* 匹配任意字符序列
  • 匹配在 AST 解析后进行,确保不会被引号嵌套等语法技巧绕过

PermissionDecisionReason

每个权限决策都附带原因说明,用于审计和调试:

  • classifier — 由分类器自动判断
  • hook — 由钩子拦截
  • rule — 由配置规则匹配
  • subcommandResults — 基于子命令分析结果
  • mode — 由当前权限模式决定
  • sandboxOverride — 沙箱覆盖
  • workingDir — 工作目录检查
  • safetyCheck — 安全检查

架构

模块关系与设计决策

权限系统架构

核心模块

模块职责关键函数/类型
utils/permissions/权限决策引擎hasPermissionsToUseTool()
bashClassifier.tsBash 命令安全分类classifyBashCommand()
yoloClassifier.ts自动模式安全分类classifyTranscript()
sandbox-adapter.ts沙箱接口适配createSandbox(), runInSandbox()
useCanUseToolUI 权限对话框React Hook + Dialog

5 层权限决策流程

  1. Layer 1: Blanket Deny → 系统硬编码的绝对禁止规则
    • 匹配 → DENY(不可覆盖)
  2. Layer 2: Always-Allow → 用户配置的自动允许
    • 匹配 → ALLOW
  3. Layer 3: Always-Deny → 用户配置的自动拒绝
    • 匹配 → DENY
  4. Layer 4: Tool checkPermissions() → 工具自身的检查逻辑
    • allow/deny/ask → 传递对应决策
  5. Layer 5: Classifier / User Prompt → 最终裁定
    • default 模式 → 弹窗让用户确认
    • auto 模式 → yoloClassifier 自动判断
    • plan 模式 → 写操作直接拒绝

PermissionMode 行为矩阵

模式只读工具写操作工具未知风险工具
default自动允许用户确认用户确认
plan自动允许直接拒绝直接拒绝
auto自动允许分类器判断分类器判断
bypassPermissions自动允许自动允许自动允许

拒绝追踪与回退

  • 追踪:记录每次分类器拒绝的工具名和原因
  • 检测:shouldFallbackToPrompting() 检查连续拒绝次数
  • 回退:超过阈值(3次),切换到用户确认模式
  • 重置:用户做出决策后,重置计数器

设计决策

为什么权限系统是 5 层而不是更简单的 allow/deny 二元决策?

单一层级无法满足不同场景的需求:系统级安全需要硬编码拒绝(第 1 层),用户偏好需要可配置规则(第 2/3 层),工具特有逻辑需要自定义检查(第 4 层),兜底策略需要分类器或人工确认(第 5 层)。每一层解决一个独立的关注点,组合在一起形成了既安全又灵活的权限体系。

源码

3 个关键代码示例

01
5 层权限决策引擎
TypeScript
// utils/permissions/hasPermissionsToUseTool.ts — 简化版

export type PermissionDecision = {
  allowed: boolean
  reason: PermissionDecisionReason
  message?: string
}

export type PermissionDecisionReason =
  | 'classifier'
  | 'hook'
  | 'rule'
  | 'subcommandResults'
  | 'mode'
  | 'sandboxOverride'
  | 'workingDir'
  | 'safetyCheck'

export async function hasPermissionsToUseTool(
  tool: Tool,
  input: unknown,
  context: ToolPermissionContext
): Promise<PermissionDecision> {
  // ===== 第 1 层:全局拒绝规则 =====
  const blanketDeny = checkBlanketDenyRules(tool, input)
  if (blanketDeny) {
    return {
      allowed: false,
      reason: 'safetyCheck',
      message: blanketDeny.message,
    }
  }

  // ===== 第 2 层:始终允许规则 =====
  if (context.alwaysAllowRules.length > 0) {
    const allowMatch = matchPermissionRule(
      tool, input, context.alwaysAllowRules
    )
    if (allowMatch) {
      return { allowed: true, reason: 'rule' }
    }
  }

  // ===== 第 3 层:始终拒绝规则 =====
  if (context.alwaysDenyRules.length > 0) {
    const denyMatch = matchPermissionRule(
      tool, input, context.alwaysDenyRules
    )
    if (denyMatch) {
      return {
        allowed: false,
        reason: 'rule',
        message: '被拒绝规则阻止: ' + denyMatch.pattern,
      }
    }
  }

  // ===== 第 4 层:工具自身权限检查 =====
  const toolPermResult = await tool.checkPermissions(input, context)
  if (toolPermResult.decided) {
    return {
      allowed: toolPermResult.allowed,
      reason: toolPermResult.reason,
      message: toolPermResult.message,
    }
  }

  // ===== 第 5 层:根据模式决定 =====
  switch (context.mode) {
    case 'plan':
      // 计划模式:只读工具允许,其余拒绝
      return tool.isReadOnly()
        ? { allowed: true, reason: 'mode' }
        : { allowed: false, reason: 'mode', message: '计划模式下不允许写操作' }

    case 'bypassPermissions':
      return { allowed: true, reason: 'mode' }

    case 'auto':
      // 自动模式:使用分类器
      return classifyForAutoMode(tool, input, context)

    case 'default':
    default:
      // 默认模式:需要用户确认
      return {
        allowed: false,
        reason: 'classifier',
        message: 'needsUserConfirmation',
      }
  }
}

function checkBlanketDenyRules(
  tool: Tool,
  input: unknown
): { message: string } | null {
  // 硬编码的绝对禁止规则
  if (tool.name === 'Bash') {
    const cmd = (input as { command: string }).command
    if (cmd.includes('rm -rf /') && !cmd.includes('rm -rf /tmp')) {
      return { message: '禁止执行可能删除根目录的命令' }
    }
  }
  return null
}
02
Bash 分类器与 YOLO 分类器
TypeScript
// bashClassifier.ts — Bash 命令安全分类
export interface BashClassification {
  riskLevel: 'safe' | 'moderate' | 'dangerous'
  category: string
  requiresConfirmation: boolean
  explanation: string
}

export function classifyBashCommand(
  command: string,
  astAnalysis: CommandAnalysis
): BashClassification {
  // 已知安全命令
  if (astAnalysis.isReadOnly && !astAnalysis.hasRedirection) {
    return {
      riskLevel: 'safe',
      category: 'read-only',
      requiresConfirmation: false,
      explanation: '只读命令,无副作用',
    }
  }

  // 已知危险命令模式
  const dangerousPatterns = [
    { pattern: /rms+(-rf?|--recursive)/, category: 'destructive-delete' },
    { pattern: /chmods+777/, category: 'insecure-permissions' },
    { pattern: />s*/etc//, category: 'system-file-write' },
    { pattern: /curl.*|s*(bash|sh)/, category: 'remote-code-execution' },
    { pattern: /gits+pushs+.*--force/, category: 'force-push' },
  ]

  for (const { pattern, category } of dangerousPatterns) {
    if (pattern.test(command)) {
      return {
        riskLevel: 'dangerous',
        category,
        requiresConfirmation: true,
        explanation: '检测到高风险操作: ' + category,
      }
    }
  }

  // 有写操作但不是已知危险的
  if (!astAnalysis.isReadOnly || astAnalysis.hasRedirection) {
    return {
      riskLevel: 'moderate',
      category: 'write-operation',
      requiresConfirmation: true,
      explanation: '命令可能修改文件系统',
    }
  }

  return {
    riskLevel: 'safe',
    category: 'unknown-safe',
    requiresConfirmation: false,
    explanation: '未检测到风险',
  }
}

// yoloClassifier.ts — 自动模式分类
export async function classifyForAutoMode(
  tool: Tool,
  input: unknown,
  context: ToolPermissionContext
): Promise<PermissionDecision> {
  // 只读工具:始终允许
  if (tool.isReadOnly()) {
    return { allowed: true, reason: 'classifier' }
  }

  // Bash 命令:使用专用分类器
  if (tool.name === 'Bash') {
    const cmd = (input as { command: string }).command
    const analysis = analyzeCommand(cmd)
    const classification = classifyBashCommand(cmd, analysis)

    if (classification.riskLevel === 'dangerous') {
      return {
        allowed: false,
        reason: 'classifier',
        message: classification.explanation,
      }
    }

    // 中等风险:检查拒绝追踪
    if (classification.riskLevel === 'moderate') {
      if (shouldFallbackToPrompting(tool.name, context)) {
        return {
          allowed: false,
          reason: 'classifier',
          message: 'needsUserConfirmation',
        }
      }
      return { allowed: true, reason: 'classifier' }
    }
  }

  // 文件写操作:检查是否在工作目录内
  if (tool.name === 'Edit' || tool.name === 'Write') {
    const filePath = (input as { file_path: string }).file_path
    if (isWithinWorkingDir(filePath, context)) {
      return { allowed: true, reason: 'workingDir' }
    }
    return {
      allowed: false,
      reason: 'workingDir',
      message: '文件不在工作目录内',
    }
  }

  // 默认:允许
  return { allowed: true, reason: 'classifier' }
}
03
Shell 规则匹配与用户确认 Hook
TypeScript
// 权限规则匹配系统

interface PermissionRule {
  pattern: string // "Bash(git *)", "Read", "Edit"
}

export function matchPermissionRule(
  tool: Tool,
  input: unknown,
  rules: PermissionRule[]
): PermissionRule | null {
  for (const rule of rules) {
    if (matchSingleRule(tool, input, rule)) {
      return rule
    }
  }
  return null
}

function matchSingleRule(
  tool: Tool,
  input: unknown,
  rule: PermissionRule
): boolean {
  // 简单工具名匹配: "Read", "Glob"
  if (!rule.pattern.includes('(')) {
    return tool.name === rule.pattern
  }

  // 带参数的匹配: "Bash(npm test*)"
  const match = rule.pattern.match(/^(\w+)\((.+)\)$/)
  if (!match) return false

  const [, toolName, commandPattern] = match
  if (tool.name !== toolName) return false

  // 对 Bash 工具,匹配命令内容
  if (toolName === 'Bash') {
    const command = (input as { command: string }).command
    return globMatch(command.trim(), commandPattern)
  }

  // 对文件工具,匹配文件路径
  if (toolName === 'Read' || toolName === 'Edit' || toolName === 'Write') {
    const filePath = (input as { file_path: string }).file_path
    return globMatch(filePath, commandPattern)
  }

  return false
}

// glob 匹配:* 匹配任意字符序列
function globMatch(text: string, pattern: string): boolean {
  // 转义正则特殊字符,然后将 glob 通配符转换为正则
  const escaped = escapeRegExp(pattern)
  const regexStr = '^' + escaped
    .replace(/\\\*/g, '.*')  // glob * → regex .*
    .replace(/\\\?/g, '.')   // glob ? → regex .
    + '$'
  return new RegExp(regexStr).test(text)
}

function escapeRegExp(str: string): string {
  return str.replace(/[.*+?^$\\|()\[\]]/g, '\\$&')
}

// 拒绝追踪与回退
const denialCounts = new Map<string, number>()

export function shouldFallbackToPrompting(
  toolName: string,
  context: ToolPermissionContext
): boolean {
  const key = toolName
  const count = denialCounts.get(key) ?? 0
  return count >= 3 // 连续拒绝 3 次后回退到用户确认
}

export function trackDenial(toolName: string): void {
  const count = denialCounts.get(toolName) ?? 0
  denialCounts.set(toolName, count + 1)
}

export function resetDenials(toolName: string): void {
  denialCounts.delete(toolName)
}

// useCanUseTool — React Hook (简化版)
export function useCanUseTool(toolCall: ToolUseBlock) {
  const [decision, setDecision] = useState<PermissionDecision | null>(null)
  const [showDialog, setShowDialog] = useState(false)

  useEffect(() => {
    async function check() {
      const result = await hasPermissionsToUseTool(
        toolCall.tool, toolCall.input, getPermissionContext()
      )

      if (result.message === 'needsUserConfirmation') {
        setShowDialog(true) // 弹出确认对话框
      } else {
        setDecision(result)
      }
    }
    check()
  }, [toolCall])

  const handleUserDecision = (
    allowed: boolean,
    alwaysAllow: boolean
  ) => {
    setDecision({ allowed, reason: 'classifier' })
    if (alwaysAllow) {
      // 将规则添加到 always-allow 列表
      addAlwaysAllowRule(toolCall.tool.name, toolCall.input)
    }
    setShowDialog(false)
  }

  return { decision, showDialog, handleUserDecision }
}

互动

步进式流程演示

互动演示

权限系统互动解析

第 1 步:理解 5 层决策的必要性

让我们用一个具体例子走过 5 层决策:Claude 调用 Bash("git push --force origin main")

  1. 第 1 层(全局拒绝):虽然危险,但不在硬编码禁止列表中 → 通过
  2. 第 2 层(始终允许):用户配置了 Bash(git *) → 匹配!但等等...这真的安全吗?

这就是 5 层设计的问题:如果用户配置了过于宽泛的 always-allow 规则,git push --force 会被自动允许。Claude Code 的文档强调:不要使用过于宽泛的规则。正确的做法是 Bash(git status*)Bash(git log*) 这样的精确规则。

第 2 步:Bash 分类器的深度分析

Bash 分类器不是简单的黑名单匹配,而是理解命令语义:

  • cat file.txt → safe(只读)
  • cat file.txt > output.txt → moderate(有重定向,可能覆盖文件)
  • cat file.txt | grep pattern → safe(管道不产生副作用)
  • $(cat file.txt) → moderate(命令替换,结果可能被执行)

关键洞察:同一个 cat 命令在不同上下文中有不同的风险等级。这就是为什么需要 AST 分析而不是简单的命令名匹配。

第 3 步:YOLO 模式的安全边界

Auto/YOLO 模式并不是"允许一切"。它的核心原则是:

  • 自动允许:与用户明确要求一致的操作("帮我编辑 config.ts" → 允许编辑 config.ts)
  • 自动拒绝:明显危险的操作(rm -rf、curl | bash)
  • 回退确认:不确定的操作连续被拒绝 3 次后,切换到人工确认

这意味着即使在 YOLO 模式下,用户仍然有安全保护。分类器的目标是"像一个有经验的开发者那样判断"。

第 4 步:沙箱的隔离机制

沙箱为不信任的命令提供了一个隔离的执行环境:

  • 文件系统隔离:命令只能看到工作目录及其子目录,无法访问 ~/.ssh、/etc 等
  • 网络隔离:可选地禁止网络访问
  • 进程隔离:命令无法提升权限或影响其他进程
  • 使用场景:执行不受信任的构建脚本、运行测试等

第 5 步:用户确认对话框的设计

权限确认对话框提供了三个选项:

  • "允许" — 本次允许,下次同类操作仍需确认
  • "拒绝" — 本次拒绝,Claude 收到错误信息并调整策略
  • "始终允许" — 将此操作模式添加到 always-allow 规则,后续自动放行

这种渐进式信任模型让用户可以:

  1. 初始保持谨慎(每次都确认)
  2. 逐渐建立信任(对常用操作设置"始终允许")
  3. 最终达到高效状态(大多数操作自动放行,只有异常操作需要确认)

关键设计洞察

  • 纵深防御:5 层决策确保即使某一层被绕过,后续层仍能拦截
  • 可配置性:用户可以通过规则精确控制权限边界
  • 渐进信任:从"全部确认"到"大部分自动"的平滑过渡
  • 安全分类:Bash 分类器理解命令语义,而非简单的模式匹配
  • 回退机制:分类器不确定时回退到人工确认,避免过度阻塞

相关源文件

utils/permissions/