一个60MB的.map文件,扒光了Anthropic 51万行代码——Claude Code泄露事件深度拆解

大家好,我是摘星,今天我们来拆解一下AI行业这几天最炸裂的事件——Anthropic的Claude Code源码泄露。

51万行TypeScript代码、1900多个源文件、40多个工具模块,因为一个npm发布时忘记删除的Source Map文件,被完整暴露在公网上。这不是黑客攻击,不是内部人举报,而是Anthropic自己把家底打包送上了npm仓库。更要命的是,这已经是Claude Code第二次犯同样的错误。2025年初上线时就因为Source Map泄露过一次源码,当时直接催化了整个Coding Agent赛道的爆发。时隔一年,Anthropic在同一块石头上又绊倒了。

这篇文章我会从三个层面来拆解这件事:第一,事件本身的技术根因和完整时间线;第二,泄露源码暴露出的Claude Code工业级Agent架构(这部分对开发者非常有参考价值);第三,这场事故给所有团队敲响的npm发布安全警钟,以及如何在CI/CD中建立自动化的防护机制。


一、事件还原:一个.map文件引发的"被动开源"

1.1 什么是Source Map?为什么它这么致命

先给不太熟悉的读者补个课。Source Map(.map文件)是前端工程化中的标准配置,它的作用是将构建后压缩、混淆的生产代码映射回原始源码。开发时开启Source Map,浏览器DevTools就能显示原始文件名、变量名、断点位置,方便调试。

问题在于,Source Map里存的是完整的原始源码——变量名、函数名、注释、目录结构,一个不落。它就像一个压缩包的解压密钥,有了它,混淆代码分分钟就能还原成可读的开发版本。

// tsconfig.json 中开启 sourceMap
{
  "compilerOptions": {
    "sourceMap": true,  // 开发环境开启
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

// 构建后生成的产物:
// dist/cli.js          ← 压缩混淆后的生产代码(可发布)
// dist/cli.js.map      ← Source Map 文件(绝对不能发布!)

上面这段配置中,sourceMap: true在开发阶段没问题,但如果构建生产版本时没有关掉,或者构建流程没有过滤掉.map文件,那.map就会跟着你的代码一起发布到npm。而npm是一个公开的包注册表,任何人都可以npm install下载你的包——包括那个不该出现的.map文件。

1.2 事件时间线

时间 事件
2025年初 Claude Code首次上线,因npm包含Source Map导致源码泄露(第一次)
2025年内 Anthropic修复发布流程,Claude Code快速发展为顶级AI编程工具
2026年3月31日 Claude Code 2.1.88版本发布到npm,cli.js.map(~60MB) 再次被打包
同日 开发者Chaofan Shou发现并在Twitter公开泄露内容
2026年4月1日 GitHub上出现大量Fork和克隆,Anthropic紧急下架并封杀
同日 Anthropic发言人确认:人为失误,非外部攻击,未涉敏感客户数据
2026年4月2日 社区完成源码分析,多篇深度技术拆解文章发布

这已经不是简单的"配置疏忽"了。同一个团队,同一个工具,同一种失误模式,重复两次——这说明Anthropic的发布流程中缺乏自动化的安全门禁。一个60MB的异常文件混入发布包,没有任何告警机制拦截它。

1.3 泄露规模

根据社区分析,这次泄露的源码快照包含:

  • 1,900+个TypeScript源文件
  • 51.2万行代码
  • 40+个内置工具模块
  • 85个斜杠命令(Slash Commands)
  • 内部API设计、遥测系统、加密工具、进程间通信协议
  • 多Agent编排机制的完整实现
  • 多个未公开功能的内部代号

一张信息图风格的插图


二、架构揭秘:从源码看Claude Code的工业级Agent设计

与其纠结于泄露本身,不如把这次"被动开源"当作一次难得的学习机会。Claude Code是目前市面上最成功的AI编程Agent之一,它的架构设计代表了这个领域的第一梯队水平。

2.1 技术栈全景

从泄露的源码中,可以清晰看到Claude Code的技术选型:

层次 技术选择 用途
运行时 Bun 替代Node.js,更快的启动和执行速度
终端UI React + Ink 用React组件的方式构建CLI界面
CLI解析 Commander.js 命令行参数和子命令处理
Schema校验 Zod v4 工具参数的类型安全校验
语言 TypeScript 全量TypeScript,类型完备

这套选型的逻辑很清晰:Bun保证性能(Agent类工具需要频繁启动子进程),React+Ink让终端界面可以组件化开发,Zod则确保LLM调用的每个工具都有严格的输入输出Schema。

2.2 Coordinator-Worker:从单线程Agent到多层编排

Claude Code最核心的架构发现,是它的Coordinator-Worker模式

默认情况下,Claude Code是一个标准的单线程Agent循环:接收用户指令→调用LLM→执行工具→返回结果→继续下一轮。这对于简单任务够用,但面对复杂的多文件重构、跨模块修改时,单线程循环的效率瓶颈非常明显。

当环境变量启用协调器模式后,整个系统被重构为两层架构:

图表

这个架构中有三个关键设计:

权限隔离:Coordinator被剥夺了直接操作文件的能力,它只能派生Worker子Agent并分配任务。这个设计非常聪明——协调者负责规划和调度,工作者负责执行,职责清晰,也降低了误操作的风险。

缓存继承:Worker启动时可以继承Coordinator已积累的上下文缓存(Prompt Cache),这意味着子Agent不需要从零开始理解任务背景,大幅减少了token消耗。

结论回传:Worker完成任务后,只向Coordinator回传精炼后的结论,而非整个思考过程。这避免了上下文窗口被大量中间信息占满。

来看看这个模式在代码层面的简化实现思路:

// 简化版 Coordinator-Worker 编排逻辑
interface AgentMessage {
  role: "coordinator" | "worker";
  content: string;
  toolCalls?: ToolCall[];
}

class CoordinatorAgent {
  private workers: Map<string, WorkerAgent> = new Map();

  async handleTask(userInstruction: string): Promise<string> {
    // 第一步:分析任务,制定执行计划
    const plan = await this.llm.chat({
      messages: [
        { role: "system", content: COORDINATOR_SYSTEM_PROMPT },
        { role: "user", content: userInstruction }
      ],
      // Coordinator 不配备文件操作工具,只有派生能力
      tools: [this.spawnWorkerTool, this.aggregateTool]
    });

    // 第二步:根据计划派生 Worker
    for (const subtask of plan.subtasks) {
      const worker = new WorkerAgent({
        inheritCache: plan.promptCache,  // 继承上下文缓存
        tools: [fileReadTool, fileWriteTool, bashTool, grepTool],
        permission: subtask.requiredPermission
      });
      this.workers.set(subtask.id, worker);
    }

    // 第三步:收集结果并整合
    const results = await Promise.all(
      [...this.workers.values()].map(w => w.execute())
    );
    return this.aggregateResults(results);
  }
}

这段代码展示了Coordinator-Worker模式的核心骨架。CoordinatorAgent只有两个工具:派生Worker和聚合结果。它不直接碰文件,不做Bash执行,纯粹是一个"调度大脑"。而WorkerAgent则配备了完整的文件操作、代码搜索、Shell执行能力,负责干活。两者的权限边界通过工具配置天然隔离。

2.3 三层记忆系统:让Agent"记住"上下文

另一个让我印象深刻的设计是Claude Code的三层记忆系统。长时运行的Agent最头疼的问题就是上下文丢失——用户让它改了10个文件,改到第11个时它已经忘了第1个改了什么。Claude Code用一种优雅的方式解决了这个问题。

第一层:CLAUDE.md文件记忆

# CLAUDE.md(项目级记忆文件,存放在项目根目录)

## 项目结构
- src/core/ — 核心Agent逻辑
- src/tools/ — 工具模块定义
- src/services/ — 运行时服务

## 编码规范
- 使用 TypeScript strict 模式
- 测试框架:Vitest
- 提交信息格式:Conventional Commits

## 已知问题
- #142: 文件监听在高并发下存在竞态条件
- #198: 大文件diff性能需要优化

CLAUDE.md是一种Markdown格式的持久化记忆文件。用户可以在里面写入项目约定、编码规范、已知问题等,Claude Code每次启动时自动读取并注入到系统提示词中。这不是什么新技术,但Claude Code把它做到了极致——支持项目级(项目根目录)、用户级(~/.claude/CLAUDE.md)和全局级三层,每层的优先级和作用域都有明确定义。

第二层:短期上下文记忆

就是Agent运行过程中的对话历史。Claude Code用了一种多层上下文压缩策略:当对话历史超过一定长度时,自动对早期的对话进行摘要压缩,保留关键决策和操作记录,丢弃中间的探索过程。

第三层:跨会话记忆提取

每次会话结束时,Claude Code会自动从对话中提取关键信息(比如用户的偏好、项目的特殊配置、重复出现的问题模式),写入CLAUDE.md文件。这样下次启动时,Agent就"记住"了之前的经验。

图表

2.4 工具系统的声明式设计

Claude Code内置了40多个工具,但添加新工具的成本极低。秘诀在于它的声明式工具定义

// Claude Code 工具定义模式(简化版)
import { z } from "zod";

const fileWriteTool = defineTool({
  name: "file_write",
  description: "将内容写入指定路径的文件",
  // Schema 定义输入参数(Zod v4 类型安全)
  inputSchema: z.object({
    path: z.string().describe("文件路径,相对于项目根目录"),
    content: z.string().describe("要写入的文件内容"),
    createDirs: z.boolean().default(false)
      .describe("如果父目录不存在是否自动创建")
  }),
  // 权限模型定义安全边界
  permissions: {
    // 文件写入需要用户确认
    requireConfirmation: true,
    // 路径白名单
    allowedPaths: ["./src", "./tests", "./docs"],
    // 路径黑名单(绝对不能写)
    blockedPaths: ["node_modules", ".git", ".env"]
  },
  // 独立模块实现具体逻辑
  execute: async ({ path, content, createDirs }, context) => {
    const resolvedPath = context.resolveProjectPath(path);
    // 安全检查:路径遍历攻击防护
    if (resolvedPath.includes("..")) {
      throw new SecurityError("路径不允许包含 ..");
    }
    if (createDirs) {
      await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
    }
    await fs.writeFile(resolvedPath, content, "utf-8");
    return { success: true, path: resolvedPath };
  }
});

这个设计模式值得每个做Agent开发的团队学习。每个工具由三个维度定义:

  1. Schema(Zod):定义输入参数的类型和约束。LLM调用工具时,参数必须通过Schema校验才能到达执行层,这是第一道防线。
  2. 权限模型:声明式定义该工具需要什么级别的权限确认。requireConfirmation表示需要用户点头,allowedPathsblockedPaths限制操作范围。
  3. 执行逻辑:独立的模块实现具体操作,内部包含安全检查(比如路径遍历攻击防护)。

这三层设计让工具系统既灵活又安全。新增工具只需要按模板定义三个维度,不用改框架代码,不用手动拼接权限检查。这种工厂模式(Factory Pattern)把"定义工具"和"运行工具"彻底解耦了。

2.5 安全机制:9300行代码构筑的四层纵深防御

源码分析显示,Claude Code有大约9300行代码专门用于安全检查。这些代码构成了一个四层纵深防御体系:

层级 机制 说明
第一层 Schema校验 Zod v4对LLM输出的工具调用参数进行严格类型校验
第二层 权限链 六级权限模型,不同工具对应不同确认级别
第三层 沙箱隔离 Bash执行在受限环境中运行,禁止危险命令
第四层 运行时监控 实时检测异常行为模式,触发熔断机制

这个安全架构的设计哲学是:永远不要信任LLM的输出。大模型可能产生幻觉、注入攻击、或者因为理解偏差执行危险操作。每一层防御都假设前面的层可能被突破,独立提供保护。

一张纵深防御架构示意


三、安全反思:如何防止Source Map泄露

Claude Code的泄露事件表面看是一个低级错误,但深层反映的是工程化流程中的系统性缺陷。一个60MB的异常文件能毫无阻碍地通过构建、测试、发布全流程,说明这条流水线上没有任何一个环节在做内容审计。

3.1 Source Map泄露的攻防本质

Source Map泄露不是新鲜事。历史上,多起知名安全事故都和Source Map有关。它的危害远超很多人的想象:

  • 完整代码逻辑暴露:不是压缩后的混淆代码,而是带着原始变量名、函数名、注释的完整源码
  • API密钥和内网地址泄露:开发者经常在代码中硬编码测试用的密钥、内网域名、数据库地址
  • 业务逻辑暴露:权限校验逻辑、加密算法实现、业务规则,全部可读
  • 供应链攻击入口:了解内部架构后,可以精准构造针对特定模块的攻击

3.2 构建层面的防护

最根本的防护是在构建配置中确保Source Map不会出现在生产产物中:

// package.json — 使用 files 白名单(推荐)
{
  "name": "my-cli-tool",
  "version": "1.0.0",
  // 只发布这些文件,其他一律排除
  "files": [
    "dist/cli.js",
    "dist/commands/",
    "README.md",
    "LICENSE"
    // 注意:没有 .map 文件!
  ]
}
# .npmignore 方式(备选方案)
# 在项目根目录创建 .npmignore
echo "**/*.map" >> .npmignore
echo "src/" >> .npmignore
echo "tsconfig.json" >> .npmignore
echo ".env*" >> .npmignore

两种方式中,files白名单比.npmignore黑名单更安全。白名单模式只包含你明确指定的文件,任何不在列表中的文件都不会被发布。而黑名单模式需要你穷举所有不该发布的文件模式,一旦漏掉某个模式(比如.map),就会中招。

3.3 CI/CD层面的自动化门禁

手动检查永远不如自动化检查可靠。在CI/CD pipeline中增加安全扫描步骤,是防止此类事故的最后一道防线:

# .github/workflows/publish.yml
name: Publish npm Package

on:
  push:
    tags: ["v*"]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: 检查构建产物中的敏感文件
        run: |
          # 使用 npm pack --dry-run 预览将要发布的文件
          npm pack --dry-run 2>&1 | tee pack-output.txt

          # 检查是否包含 .map 文件
          if grep -q "\.map" pack-output.txt; then
            echo "::error::发布包中包含 .map 文件!"
            echo "以下 .map 文件将被发布:"
            grep "\.map" pack-output.txt
            exit 1
          fi

          # 检查包大小是否异常(超过20MB告警)
          PACK_SIZE=$(grep "package size:" pack-output.txt | grep -oP '\d+.*B')
          echo "发布包大小: $PACK_SIZE"

          # 检查是否包含 .env 文件
          if grep -q "\.env" pack-output.txt; then
            echo "::error::发布包中包含 .env 文件!"
            exit 1
          fi

      - name: 发布到 npm
        if: success()
        run: npm publish

这个CI配置做了三件事:第一,用npm pack --dry-run预览发布内容,不实际生成文件;第二,检查是否存在.map文件和.env文件,发现就中断流程;第三,监控包大小,60MB的异常大文件会触发告警。如果Anthropic有类似的机制,Claude Code的第二次泄露完全可以避免。

3.4 发布后的验证机制

// scripts/verify-publish.ts — 发布后自动验证
import { execSync } from "child_process";
import { readFileSync } from "fs";

const PKG = JSON.parse(readFileSync("package.json", "utf-8"));

async function verifyPublishedPackage() {
  // 下载刚发布的包到临时目录
  execSync(`npm pack ${PKG.name}@${PKG.version}`, { stdio: "inherit" });

  const tarball = `${PKG.name}-${PKG.version}.tgz`;
  execSync(`tar -tzf ${tarball}`, { stdio: "inherit" });

  // 验证包中不包含 .map 文件
  const contents = execSync(`tar -tzf ${tarball}`).toString();
  const mapFiles = contents.split("\n").filter(f => f.endsWith(".map"));

  if (mapFiles.length > 0) {
    console.error(`❌ 发现 ${mapFiles.length} 个 .map 文件:`);
    mapFiles.forEach(f => console.error(`  - ${f}`));
    process.exit(1);
  }

  console.log("✅ 发布验证通过,未发现敏感文件");
}

verifyPublishedPackage();

发布后立即下载并验证包内容,这是一个"最后一英里"的检查。它和CI中的pre-publish检查形成双保险:即使CI被绕过(比如手动npm publish),发布后验证也能发现问题。


四、从泄露事件看Agent开发的安全挑战

Claude Code的源码泄露之所以引发行业震动,不仅因为Anthropic是AI安全领域的标杆公司,更因为它暴露了一个更深层的矛盾:AI Agent越强大,对安全的挑战就越大

4.1 "信任大模型输出"是一个危险假设

Claude Code的四层纵深防御架构,底层假设就是"永远不要信任LLM的输出"。这不仅仅是防御外部的Prompt Injection攻击,也是在防御大模型自身的理解偏差。

举个实际的例子:用户让Agent"删除测试文件",LLM可能把它理解成rm -rf ./tests/,也可能理解成rm -rf /test/。在单线程模式下,这种偏差还能通过用户确认来拦截。但在Coordinator-Worker模式下,Worker是自动执行的——如果Coordinator的指令有歧义,Worker可能做出不可逆的操作。

这就是为什么Claude Code要设计9300行安全代码。不是为了炫技,而是因为大模型输出的不确定性要求每一层操作都必须有独立的安全校验。

4.2 Agent架构中的权限最小化原则

Claude Code的Coordinator-Worker架构体现了一个关键的安全原则——最小权限。Coordinator作为"大脑"却没有手(不能操作文件),Worker作为"手"却没有脑(不能自主决策),两者互相制约。

这种设计对其他Agent开发者有直接的参考价值:

// 工具权限分级示例
enum PermissionLevel {
  NONE = "none",           // 不需要确认(如读取文件)
  CONFIRM_ONCE = "once",   // 首次使用时确认(如搜索代码)
  CONFIRM_ALWAYS = "always", // 每次使用都确认(如写入文件、执行Bash)
  BLOCKED = "blocked"       // 禁止使用(如删除目录、修改系统文件)
}

interface ToolPermission {
  tool: string;
  level: PermissionLevel;
  constraints?: {
    maxFileSize?: number;      // 文件大小限制
    allowedExtensions?: string[]; // 允许的文件类型
    blockedCommands?: string[];  // 禁止的Shell命令
    rateLimit?: number;          // 调用频率限制
  };
}

// 权限配置表
const PERMISSION_CONFIG: ToolPermission[] = [
  { tool: "file_read",   level: PermissionLevel.NONE },
  { tool: "file_write",  level: PermissionLevel.CONFIRM_ALWAYS,
    constraints: { maxFileSize: 1024 * 1024 } },  // 1MB
  { tool: "bash_exec",   level: PermissionLevel.CONFIRM_ALWAYS,
    constraints: { blockedCommands: ["rm -rf /", "mkfs", "dd if="] } },
  { tool: "net_request", level: PermissionLevel.CONFIRM_ONCE },
  { tool: "sys_modify",  level: PermissionLevel.BLOCKED },
];

这个权限分级体系让Agent在保持灵活性的同时控制住了风险。无风险操作(读文件)自动放行提升效率,高风险操作(写文件、执行Shell)必须经过确认,禁止操作直接拦截。

4.3 供应链安全的系统性思考

Claude Code泄露事件的教训可以总结为一句话:安全不是某一个环节的责任,而是整个流程的属性

一个完整的npm发布安全体系需要覆盖三个阶段:

图表

Anthropic的失误在于:构建阶段没有过滤Source Map,CI阶段没有内容扫描,发布阶段没有大小告警。三道防线全部失守,一个60MB的文件就畅通无阻地上了npm。


五、给开发者的行动清单

如果你在开发CLI工具或任何通过npm分发的产品,以下是立刻可以执行的检查项:

立即检查(5分钟)

  1. 运行npm pack --dry-run,看看你的包里都有什么
  2. 检查package.json中是否有files白名单字段
  3. 搜索构建产物中是否存在.map文件

本周完成

  1. package.json中添加files白名单,只包含必要文件
  2. 在CI/CD中添加.map文件检测步骤
  3. 设置npm包大小告警阈值

持续执行

  1. 每次发布前运行安全扫描
  2. 定期轮换npm发布token
  3. 开启npm双因素认证

Anthropic有世界上顶尖的安全研究团队,他们的模型安全对齐工作走在行业前列。但这件事告诉我们:再强的安全团队也防不住工程流程中的基础漏洞。安全不仅是模型层面的对齐问题,更是工程实践中的每一个细节。


参考资料

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐