2026 年 3 月 31 日,安全研究员 Chaofan Shou 发现 npm 上的 Claude Code 包里带了一个不该存在的 .map 文件。几小时内社区就还原出了完整源码,1900+ 个文件,51.2 万行 TypeScript,全摊在了 GitHub 上。
说白了,这事儿就是一个构建配置的疏忽。但这个疏忽的后果,比大多数人想象的要大得多。
如果你做过前端开发,Source Map 你肯定不陌生。没做过也没关系,我用一句话解释:Source Map 是一种 JSON 格式的映射文件(通常以 .map 结尾),它记录了编译打包后的代码和原始源码之间的对应关系。
为什么需要这个东西?因为现代 TypeScript / JavaScript 项目在发布之前,几乎都要经过一道构建流程。TypeScript 编译成 JavaScript,多个文件打包成一个 bundle,代码被压缩混淆以减小体积。最终用户拿到的是一坨人类几乎无法阅读的代码。Source Map 的作用就是在调试时,把这坨代码"还原"回开发者写的原始样子。
关键来了。Source Map 文件里有一个字段叫 sourcesContent,它存的就是每个源文件的完整原始文本。这不是 bug,这是 Source Map v3 规范 定义的标准行为。一个典型的 .map 文件结构长这样:
{ "version": 3, "sources": ["src/main.ts", "src/utils.ts", "..."], "sourcesContent": ["// 完整的 main.ts 源码...", "// 完整的 utils.ts 源码...", "..."], "mappings": "AAAA,SAAS..." }看到了吗?sourcesContent 数组里,每个元素就是一个源文件的完整文本。只要拿到这个 JSON,你甚至不需要理解 mappings 字段的编码规则,直接按下标取内容就行。换句话说,Source Map 本身就是源码的另一种打包形式。
但这个东西本来只应该出现在开发环境里,生产环境发布的 npm 包里不应该带它。这就像你把家门钥匙贴在门框上方便进出,结果搬家的时候忘了拿下来,新租客直接用这把钥匙进了你原来的家。
那它是怎么泄露的?这个问题的答案,跟 Claude Code 的构建工具有关。

Claude Code 使用 Bun 作为构建工具。Bun 是一个用 Zig 语言编写的高性能 JavaScript 运行时,同时兼具包管理器、打包工具和测试运行器。它的 bundler 在执行 bun build 时,sourcemap 选项 支持 "none"、"linked"、"inline" 和 "external" 四种模式。其实吧,Bun 的 sourcemap 默认值并不是"自动开启",但如果构建脚本里显式配置了 sourcemap 生成(比如为了开发调试),而 .npmignore 或 package.json 的 files 字段又没有把 .map 文件排除在外,这个文件就会随着 npm publish 一起被推送到 npm 仓库。
事件的时间线很清晰。2026 年 3 月 31 日,Bun bundler 将 1900+ 个 TypeScript 源文件打包成单一的 cli.js 入口文件,同时生成了对应的 cli.js.map,体积大约 60MB。npm 包发布时,.map 文件未被排除,随构建产物一起上传。安全研究员 Chaofan Shou 注意到了这个文件,在 X(Twitter)上公开了发现,帖子几小时内就获得了超过 310 万次浏览。社区迅速行动,Hacker News 和 Reddit 上的讨论迅速引爆,几个 GitHub 镜像仓库在当天就上线了。
还原的过程甚至不需要什么高级技术。一段十几行的脚本就够了:
import { readFileSync, writeFileSync, mkdirSync } from 'fs'; import { dirname, join } from 'path'; const sourceMap = JSON.parse(readFileSync('cli.js.map', 'utf-8')); sourceMap.sources.forEach((source: string, i: number) => { const content = sourceMap.sourcesContent[i]; if (content == null) return; const outPath = join('restored', source); mkdirSync(dirname(outPath), { recursive: true }); writeFileSync(outPath, content); }); // 就这么简单,1900+ 个文件全部还原最讽刺的地方在哪儿?泄露的源码里有一个叫 Undercover Mode 的子系统,专门用来防止 Anthropic 的内部信息泄露。他们花了大量工程精力构建了一套机制,防止 AI 在 git commit 里意外暴露内部代号,结果整个源码随一个 .map 文件直接全送了。这种"自己跟自己斗智斗勇"的操作,实在太真实了。
而且这还不是第一次。多个报道指出,2025 年初就出过一次类似的 Source Map 泄露,当时 Anthropic 悄悄撤了包修了问题。这次又犯了同样的错,社区的反应就没那么客气了。
那还原出来的代码到底长什么样?
当我们把还原出来的代码铺开来看,这些数字相当震撼。整个项目包含 1884 个 TypeScript 源文件(.ts + .tsx),总代码行数 512,664 行,项目大小约 34 MB,顶级目录 30+ 个。内置了 40 个可供 AI 调用的工具,CLI 命令(含子命令)多达 101 个,React 组件文件 389 个。其中最核心的模块是 QueryEngine.ts,单文件约 46,000 行,负责所有 LLM API 调用、流式处理、缓存和编排。工具基类 Tool.ts 约 29,000 行。
其实吧,这个规模已经不是一个"CLI 工具"了。这是一个完整的、生产级的软件系统。

最引人注目的是 main.tsx,整个项目的单一主入口文件,4683 行代码,磁盘体积约 785 KB。这个文件是整个 CLI 应用的大脑:从命令行参数解析到 OAuth 认证流程,从模型选择到 REPL 启动,几乎所有顶层逻辑都集中在这里。光看前几行就能感受到团队对启动性能的极致关注:
import { profileCheckpoint } from './utils/startupProfiler.js'; profileCheckpoint('main_tsx_entry'); import { startMdmRawRead } from './utils/settings/mdm/rawRead.js'; startMdmRawRead(); // 并行启动 MDM 子进程 import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js'; startKeychainPrefetch(); // 并行预取 macOS 钥匙串入口文件的前 20 行就做了三件事:标记性能检查点、并行启动系统子进程、预取密钥。注释里甚至精确标注了毫秒级优化目标。对于一个 CLI 工具来说,启动速度就是生命线,没人愿意每次敲一个命令等上好几秒。
但规模只是表面。真正有意思的是技术选型背后的思路。
Bun 在 JavaScript 社区的定位一直比较微妙。大多数团队还停留在"试一试"阶段,真正在核心产品上 all-in 的少之又少。Anthropic 敢在 Claude Code 上深度绑定 Bun,说明他们对 Bun 的稳定性和性能优势做了充分的评估。从源码中 import { feature } from 'bun:bundle' 这样的 Bun 专有 API 调用来看,这不是"兼容性使用",是真的 all-in。
选择 Bun 带来的好处很直接。更快的启动速度(CLI 工具的核心指标),内置的 TypeScript 支持(不需要额外的编译步骤),统一的工具链(bundler + test runner + package manager 全包了)。代价是与 Node.js 生态的部分不兼容。但从源码来看,Anthropic 的工程团队显然觉得这个权衡值得。
我自己用 Claude Code 用了大半年,一个最直观的感受就是它启动很快。现在看到源码,才知道快的原因不只是 Bun 本身,还有入口文件里那些并行预加载的设计。