第 16 课:高级权限 — 自动模式与安全防护

2 阅读9分钟

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


学习目标

完成本课后,你将能够:

  1. 解释 Auto 模式分类器的两阶段判定流程及其 transcript 构建方式
  2. 说明 YOLO 分类器的系统提示组装与用户自定义规则替换机制
  3. 列举 Bash 命令的路径遍历防护手段(URL 编码、Unicode、反斜杠、大小写)
  4. 描述 bashSecurity.ts 中的安全验证链与 shell 元字符检测
  5. 理解拒绝追踪(Denial Tracking)的熔断机制

16.1 Auto 模式概览

Auto 模式是 Claude Code 最复杂的权限模式。当工具调用需要用户确认时(behavior: 'ask'),auto 模式不弹窗,而是调用 ML 分类器来判定是否安全。

触发条件

// utils/permissions/permissions.ts — hasPermissionsToUseTool
if (
  feature('TRANSCRIPT_CLASSIFIER') &&
  (appState.toolPermissionContext.mode === 'auto' ||
   (appState.toolPermissionContext.mode === 'plan' &&
    autoModeStateModule?.isAutoModeActive()))
) {
  // 进入 auto 模式分类器流程
}

两个入口:

  1. 权限模式显式设为 auto
  2. 处于 plan 模式但 auto mode 被激活(用于 plan-to-auto 渐进切换)

分类器决策流

工具调用  ask 决策
  
  ├─ safetyCheck 且不可自动批准?
    └─ 跳过分类器  保持 ask / deny
  
  ├─ 工具需要用户交互?(requiresUserInteraction)
    └─ 跳过分类器  保持 ask
  
  ├─ 检查拒绝追踪阈值
    └─ 超限?  fallback  prompting
  
  └─ classifyYoloAction()
     
     ├─ shouldBlock: false  allow(自动批准)
       └─ 重置连续拒绝计数
     
     ├─ shouldBlock: true  deny(自动拒绝)
       ├─ 记录拒绝
       ├─ 更新拒绝追踪
       └─ 发送 UI 通知
     
     └─ unavailable  fallback
        ├─ transcriptTooLong  fallback  prompting
        └─ API 错误  可能 fail-closed

16.2 YOLO 分类器 — Transcript 构建

utils/permissions/yoloClassifier.ts 实现了 auto 模式的核心分类器。

Transcript 条目

分类器需要看到对话历史来理解当前操作的上下文:

export type TranscriptEntry = {
  role: 'user' | 'assistant'
  content: TranscriptBlock[]
}

type TranscriptBlock =
  | { type: 'text'; text: string }
  | { type: 'tool_use'; name: string; input: unknown }

buildTranscriptEntries — 提取对话上下文

export function buildTranscriptEntries(messages: Message[]): TranscriptEntry[] {
  // 遍历所有消息:
  // - user 消息 → 提取文本块
  // - assistant 消息 → 只提取 tool_use 块(排除文本!)
  // - attachment(queued_command) → 提取为 user 转
  //
  // 关键安全决策:assistant 文本被排除!
  // 因为模型文本可以被精心构造来影响分类器判定
}

toAutoClassifierInput — 工具输入投影

每个工具定义一个 toAutoClassifierInput 方法,将工具输入压缩为分类器需要看到的最小信息:

原始输入:
{
  command: "git diff HEAD~3",
  timeout: 30000,
  description: "..."
}

→ toAutoClassifierInput() →

投影输出:
"git diff HEAD~3"   // 只保留命令本身
function toCompactBlock(block, role, lookup): string {
  if (block.type === 'tool_use') {
    const tool = lookup.get(block.name)
    // 安全:try/catch 保护,历史中可能有格式错误的输入
    let encoded
    try {
      encoded = tool.toAutoClassifierInput(input) ?? input
    } catch {
      encoded = input  // fallback 到原始输入
    }
    if (encoded === '') return ''  // 工具返回空串 → 跳过
    return `${block.name} ${encoded}\n`
  }
  if (block.type === 'text' && role === 'user') {
    return `User: ${block.text}\n`
  }
  return ''
}

紧凑 Transcript 格式

User: 请帮我清理临时文件
Bash ls /tmp/my-project/
Bash find /tmp/my-project -name "*.tmp"
User: 好的,删掉它们
Bash rm /tmp/my-project/*.tmp    ← 当前要分类的操作

16.3 分类器系统提示

提示组装

export async function buildYoloSystemPrompt(
  context: ToolPermissionContext,
): Promise<string> {
  // 1. 基础提示(从 .txt 文件加载)
  const systemPrompt = BASE_PROMPT.replace(
    '<permissions_template>',
    () => isUsingExternalPermissions()
      ? EXTERNAL_PERMISSIONS_TEMPLATE
      : ANTHROPIC_PERMISSIONS_TEMPLATE,
  )

  // 2. 合并 allow/deny 规则
  const allowDescriptions = [
    ...getBashPromptAllowDescriptions(context),  // Bash 前缀规则
    ...(autoMode?.allow ?? []),                   // 用户自定义 allow
  ]
  const denyDescriptions = [
    ...getBashPromptDenyDescriptions(context),
    ...(autoMode?.soft_deny ?? []),
  ]

  // 3. 替换模板中的标记
  return systemPrompt
    .replace(/<user_allow_rules_to_replace>.../, userAllow ?? defaults)
    .replace(/<user_deny_rules_to_replace>.../, userDeny ?? defaults)
    .replace(/<user_environment_to_replace>.../, userEnvironment ?? defaults)
}

规则替换机制

用户在 settings.jsonautoMode 字段可自定义分类器行为:

{
  "autoMode": {
    "allow": [
      "Git commands that only affect local state",
      "npm/yarn install, test, build, lint"
    ],
    "soft_deny": [
      "Commands that modify system configuration",
      "Network requests to unknown hosts"
    ],
    "environment": [
      "This is a Node.js project",
      "Using PostgreSQL database locally"
    ]
  }
}

这些规则会 替换(外部模板)或 追加(Anthropic 模板)到分类器系统提示中。

CLAUDE.md 注入

分类器还会读取项目的 CLAUDE.md 作为上下文:

function buildClaudeMdMessage(): Anthropic.MessageParam | null {
  const claudeMd = getCachedClaudeMdContent()
  if (claudeMd === null) return null
  return {
    role: 'user',
    content: [{
      type: 'text',
      text: `The following is the user's CLAUDE.md configuration...
<user_claude_md>\n${claudeMd}\n</user_claude_md>`,
      cache_control: getCacheControl({ querySource: 'auto_mode' }),
    }],
  }
}

两阶段 XML 分类器

┌──────── Stage 1: Fast Path ────────┐
│                                     │
│  发送 transcript + 操作描述         │
│  追加 S1 后缀 → 要求立即决策       │
│  模型快速回答 <allow/><deny/>  │
│                                     │
│  高置信度? → 直接返回结果           │
│  低置信度? → 进入 Stage 2          │
└─────────────────────────────────────┘
           │ 需要深度思考
           ▼
┌──────── Stage 2: Thinking ─────────┐
│                                     │
│  启用 extended thinking             │
│  模型深度推理后给出判定             │
│  返回最终结果                       │
│                                     │
└─────────────────────────────────────┘

分类器响应通过工具调用返回结构化结果:

const YOLO_CLASSIFIER_TOOL_SCHEMA = {
  name: 'classify_result',
  input_schema: {
    properties: {
      thinking: { type: 'string' },     // 推理过程
      shouldBlock: { type: 'boolean' },  // 是否阻止
      reason: { type: 'string' },        // 决策原因
    },
    required: ['thinking', 'shouldBlock', 'reason'],
  },
}

16.4 Bash 安全验证链

tools/BashTool/bashSecurity.ts 实现了一条长长的安全验证链,在权限检查之前运行。

验证管线概览

bashCommandIsSafe_DEPRECATED(command)
  │
  ├─ 1. validateEmpty              → 空命令直接 allow
  ├─ 2. validateIncompleteCommands → 片段检测(以 tab/flag/operator 开头)
  ├─ 3. validateSafeCommandSubstitution → 安全 heredoc 模式
  ├─ 4. validateJqSystemFunction   → jq system() 函数调用
  ├─ 5. validateJqFileArguments    → jq 文件参数
  ├─ 6. validateObfuscatedFlags    → 混淆的命令行标志
  ├─ 7. validateShellMetacharacters → Shell 元字符
  ├─ 8. validateDangerousVariables  → 危险环境变量
  ├─ 9. validateNewlines           → 命令中的换行符
  ├─ 10. validateDangerousPatterns  → 命令替换、重定向等
  ├─ 11. validateIfsInjection      → IFS 变量注入
  ├─ 12. validateGitCommitSubstitution → git commit -m 中的替换
  ├─ 13. validateProcEnvironAccess → /proc/environ 访问
  ├─ 14. validateMalformedTokens   → 畸形 token 注入
  ├─ 15. validateBackslashEscapedWhitespace → 转义空白字符
  ├─ 16. validateBraceExpansion    → 花括号展开
  ├─ 17. validateControlCharacters → 控制字符
  ├─ 18. validateUnicodeWhitespace → Unicode 空白字符
  ├─ 19. validateMidWordHash       → 词中 # 号
  ├─ 20. validateZshDangerousCommands → Zsh 危险命令
  ├─ 21. validateBackslashEscapedOperators → 转义运算符
  ├─ 22. validateCommentQuoteDesync → 注释/引号反同步
  └─ 23. validateQuotedNewline     → 引号内换行

命令替换检测

这是最关键的检测之一。以下模式全部被阻止:

const COMMAND_SUBSTITUTION_PATTERNS = [
  { pattern: /<\(/,  message: 'process substitution <()' },
  { pattern: />\(/,  message: 'process substitution >()' },
  { pattern: /=\(/,  message: 'Zsh process substitution =()' },
  { pattern: /\$\(/, message: '$() command substitution' },
  { pattern: /\$\{/, message: '${} parameter substitution' },
  { pattern: /\$\[/, message: '$[] legacy arithmetic expansion' },
  { pattern: /~\[/,  message: 'Zsh-style parameter expansion' },
  // ... 更多模式
]

Zsh 危险命令黑名单

const ZSH_DANGEROUS_COMMANDS = new Set([
  'zmodload',   // 加载任意模块 → 文件I/O、网络、执行
  'emulate',    // emulate -c 等价于 eval
  'sysopen',    // 精细文件控制
  'sysread', 'syswrite', 'sysseek',
  'zpty',       // 伪终端执行
  'ztcp',       // TCP 连接(可用于数据泄露)
  'zsocket',    // Unix/TCP socket
  'zf_rm', 'zf_mv', 'zf_ln', 'zf_chmod', // 内建文件操作
  'zf_chown', 'zf_mkdir', 'zf_rmdir', 'zf_chgrp',
])

16.5 路径遍历防护

tools/BashTool/pathValidation.ts 实现了多层路径安全检查。

路径提取器(PATH_EXTRACTORS)

每个支持路径操作的命令都有专用的路径提取器:

export const PATH_EXTRACTORS: Record<PathCommand, (args: string[]) => string[]> = {
  cd: args => (args.length === 0 ? [homedir()] : [args.join(' ')]),
  ls: args => { /* 过滤标志,默认 '.' */ },
  rm: filterOutFlags,
  grep: args => { /* 跳过模式参数,提取路径 */ },
  git: args => { /* 只处理 git diff --no-index */ },
  // ... 34 个命令
}

POSIX -- 端选项分隔符

这是一个关键安全修复。攻击者可以用 rm -- -/../.claude/settings.json 绕过以 - 开头的路径过滤:

function filterOutFlags(args: string[]): string[] {
  const result: string[] = []
  let afterDoubleDash = false
  for (const arg of args) {
    if (afterDoubleDash) {
      result.push(arg)      // -- 之后的所有参数都是路径
    } else if (arg === '--') {
      afterDoubleDash = true
    } else if (!arg?.startsWith('-')) {
      result.push(arg)
    }
  }
  return result
}

路径验证流程

validateCommandPaths(command, args, cwd, context)
  │
  ├─ 提取路径: PATH_EXTRACTORS[command](args)
  │
  ├─ 命令验证器(如有):阻止 mv/cp 的 flag
  │
  ├─ cd + 写操作检查:
  │  └─ 复合命令含 cd + write → 阻止
  │     // 防止: cd .claude/ && mv test.txt settings.json
  │
  └─ 对每个路径:
     └─ validatePath(path, cwd, context, operationType)
        ├─ 路径标准化(expandTilde, resolve)
        ├─ URL 编码检测(%2e%2e → ..)
        ├─ Unicode 标准化
        ├─ 反斜杠注入检测
        ├─ 大小写标准化
        ├─ 检查是否在允许的工作目录内
        ├─ 检查是否是受保护文件
        └─ 检查 deny 规则

危险删除路径检测

function checkDangerousRemovalPaths(command, args, cwd): PermissionResult {
  for (const path of paths) {
    const absolutePath = isAbsolute(cleanPath) ? cleanPath : resolve(cwd, cleanPath)
    
    if (isDangerousRemovalPath(absolutePath)) {
      return {
        behavior: 'ask',
        message: `Dangerous ${command} operation detected: '${absolutePath}'
This command would remove a critical system directory.`,
        suggestions: [],  // 不提供"保存规则"选项!
      }
    }
  }
}

操作类型分类

每个命令被分为三类操作类型,影响权限检查的严格程度:

export const COMMAND_OPERATION_TYPE: Record<PathCommand, FileOperationType> = {
  // 读操作 — 最宽松
  cat: 'read', ls: 'read', grep: 'read', find: 'read', git: 'read',
  
  // 创建操作 — 中等
  mkdir: 'create', touch: 'create',
  
  // 写操作 — 最严格
  rm: 'write', mv: 'write', cp: 'write', sed: 'write',
}

16.6 拒绝追踪(Denial Tracking)

utils/permissions/denialTracking.ts 实现了一个简洁但关键的熔断机制。

状态结构

export type DenialTrackingState = {
  consecutiveDenials: number  // 连续拒绝次数
  totalDenials: number        // 总拒绝次数
}

export const DENIAL_LIMITS = {
  maxConsecutive: 3,    // 连续 3 次拒绝 → 熔断
  maxTotal: 20,         // 总计 20 次拒绝 → 熔断
} as const

不可变状态更新

export function recordDenial(state: DenialTrackingState): DenialTrackingState {
  return {
    ...state,                                        // 不可变更新
    consecutiveDenials: state.consecutiveDenials + 1,
    totalDenials: state.totalDenials + 1,
  }
}

export function recordSuccess(state: DenialTrackingState): DenialTrackingState {
  if (state.consecutiveDenials === 0) return state  // 无变化 → 返回同引用
  return {
    ...state,
    consecutiveDenials: 0,  // 成功重置连续计数,但不重置总数
  }
}

熔断条件

export function shouldFallbackToPrompting(state: DenialTrackingState): boolean {
  return (
    state.consecutiveDenials >= DENIAL_LIMITS.maxConsecutive ||
    state.totalDenials >= DENIAL_LIMITS.maxTotal
  )
}

熔断流程

auto 模式运行中...

  操作 1: 分类器拒绝 → consecutive=1, total=1
  操作 2: 分类器拒绝 → consecutive=2, total=2
  操作 3: 分类器拒绝 → consecutive=3, total=3

  shouldFallbackToPrompting() → true!
  
  ┌─────────────────────────────────┐
  │ 熔断触发:回退到用户交互模式    │
  │ 不再调用分类器,直接弹窗询问   │
  └─────────────────────────────────┘

  用户手动批准 → consecutive 重置为 0
  恢复 auto 模式分类器...

或者总拒绝达标后:

  ... 各种操作,累计 20 次拒绝 ...
  
  shouldFallbackToPrompting() → true!(永久熔断直到会话重启)

设计意图

这个机制防止以下问题:

  1. 死循环: 模型反复尝试被拒绝的操作
  2. 用户无感知: auto 模式下用户看不到拒绝,不知道模型在"空转"
  3. 资源浪费: 每次分类器调用都消耗 API token

16.7 引号提取与安全分析

bashSecurity.ts 中一个精妙的函数用于提取引号外的内容进行安全检查:

function extractQuotedContent(command: string, isJq = false): QuoteExtraction {
  let withDoubleQuotes = ''     // 保留双引号内容
  let fullyUnquoted = ''        // 完全去引号
  let unquotedKeepQuoteChars = '' // 去内容但保留引号字符
  let inSingleQuote = false
  let inDoubleQuote = false
  let escaped = false

  for (let i = 0; i < command.length; i++) {
    const char = command[i]
    
    if (escaped) {
      escaped = false
      if (!inSingleQuote) withDoubleQuotes += char
      if (!inSingleQuote && !inDoubleQuote) fullyUnquoted += char
      continue
    }
    
    if (char === '\\' && !inSingleQuote) {
      escaped = true
      // ... 传递转义字符
      continue
    }
    
    if (char === "'" && !inDoubleQuote) {
      inSingleQuote = !inSingleQuote
      unquotedKeepQuoteChars += char  // 保留引号字符本身
      continue
    }
    // ... 双引号类似处理
  }
  
  return { withDoubleQuotes, fullyUnquoted, unquotedKeepQuoteChars }
}

三种提取模式的用途:

原始命令:  echo 'safe text' "rm -rf /" `date`

withDoubleQuotes:     echo  "rm -rf /" `date`
                      ↑ 单引号内容被移除,检测双引号和反引号注入

fullyUnquoted:        echo   `date`
                      ↑ 所有引号内容移除,检测裸露的命令替换

unquotedKeepQuoteChars: echo ''  '' `date`
                        ↑ 保留引号字符,检测 'x'# 等引号邻接模式

安全重定向剥离

function stripSafeRedirections(content: string): string {
  return content
    .replace(/\s+2\s*>&\s*1(?=\s|$)/g, '')       // 2>&1
    .replace(/[012]?\s*>\s*\/dev\/null(?=\s|$)/g, '') // > /dev/null
    .replace(/\s*<\s*\/dev\/null(?=\s|$)/g, '')       // < /dev/null
}
// 注意:每个模式都有 (?=\s|$) 边界检查
// 防止 > /dev/nullo 被误匹配为 > /dev/null + 'o'

16.8 安全 Heredoc 检测

isSafeHeredoc 是一个精密的函数,判断 $(cat <<'EOF'...EOF) 模式是否安全:

安全模式:
  git commit -m "$(cat <<'EOF'
  这是提交信息
  EOF
  )"

检查条件:
  1. 分隔符必须被单引号/转义 → 内容是字面量
  2. 结束分隔符必须独占一行 → 精确匹配 bash 行为
  3. $() 不能在命令名位置 → 防止 heredoc 内容成为命令
  4. 剩余文本只能包含安全字符
  5. 不能有嵌套 heredoc → 防止索引破坏攻击
  6. 剩余文本必须通过所有其他验证器

这个函数是一个 EARLY-ALLOW 路径 — 返回 true 会绕过所有后续验证器。因此检查极其严格。


课后练习

练习 1:分类器 Transcript 构建

给定以下对话历史,手动构建分类器看到的 transcript:

用户: "帮我部署到生产环境"
模型: [text: "好的,我来执行部署"] [tool_use: Bash("npm run build")]
模型: [tool_use: Bash("npm run deploy:prod")]  ← 当前操作

思考:为什么 assistant 文本被排除?

练习 2:路径攻击分析

分析以下攻击向量,说明 pathValidation.ts 如何防御:

  • rm -- -/../.claude/settings.json
  • cat %2e%2e/%2e%2e/etc/passwd
  • ls .cLauDe/Settings.locaL.json
  • cd .claude/ && mv test.txt settings.json

练习 3:拒绝追踪状态机

画出 DenialTrackingState 的状态转换图,标注:

  • 初始状态 (0, 0)
  • 连续拒绝 3 次后的状态
  • 中间成功一次后的状态
  • 总计达到 20 次后的状态

练习 4:安全验证绕过

阅读 bashCommandIsSafe_DEPRECATED 的 22 个验证器。找出一个"先返回的验证器会掩盖后续验证器"的场景。思考这是否构成安全问题。


本课小结

要点内容
Auto 模式feature-gated ML 分类器,两阶段判定(fast + thinking)
Transcript用户文本 + 助手工具调用(排除助手文本),JSONL 紧凑格式
系统提示BASE_PROMPT + 权限模板 + 用户自定义 allow/deny/environment
Bash 安全22+ 验证器链,覆盖命令替换、元字符、Zsh 模块等
路径防护URL 编码、Unicode、反斜杠、大小写标准化、-- 分隔符
拒绝追踪连续 3 次或总计 20 次 → 熔断回退到用户交互
引号提取三模式(保留双引号、完全去引号、保留引号字符)精确分析

下一课预告

第 17 课:Prompt 组装架构 — 深入 constants/prompts.tsgetSystemPrompt() 函数,理解静态节(可缓存)与动态节(按需计算)的分界线、缓存策略、以及 System Prompt 如何被拆分为数组发送给 API。