request-free-img

Claude Code 源码泄露事件:一个 .map 文件如何导致 51 万行 TypeScript 代码公开

2026 年 3 月 31 日凌晨,一位安全研究员在 X 上发了一条消息:

“Anthropic 刚刚把 Claude Code 的完整源码发到 npm 上了。”

Source: https://www.digit.in/features/general/claude-code-source-code-leaked-by-anthropic-heres-what-we-know.html

不是黑客攻击,不是内鬼,就是一次普通的版本发布。

51 万行 TypeScript 源代码,共 1900 个文件,就这样躺在公开的 npm 仓库里,任何人都能下载。

而原因仅仅是:一个 .map 文件。

为什么一个 .map 文件会导致源码泄露?

今天,我们就来了解其中的原理。

Claude Code 的构建流程

Claude Code 是一个用 TypeScript 写的 CLI 工具。

这类项目通常的构建流程是,首先把所有 .ts 文件编译成 .js,同时使用工具压缩混淆(minify)源代码,使变量名变成类似于 a、b、c 这样,全部代码会被挤成一行,去掉注释、空格,从而使代码几乎无法阅读。

最终发布到 npm 的文件夹里只有又小又丑的被压缩 JS 文件。

.map 文件到底是什么?

.map 文件(全名 Source Map)是专门用来“记住”压缩前长什么样子的文件。

它是一个 JSON 文件,里面主要包含三样东西:

  • sources:用于指定原始文件名列表。
  • mappings:它是一长串采用 VLQ 编码,超级压缩的字符串,精确记录了压缩后每一行、每一列对应原始文件的哪一行、哪一列。
  • sourcesContent:是最关键的一个 key,它会直接把原始完整的源代码字符串全部塞进这个 JSON 里,而且很多构建工具默认会开启这个选项。

真实示例演示:.map 文件如何泄露源码

在我们这个示例中,hello.ts 是我们的源代码,内容如下:

// hello.ts
console.log("Hello, World from TypeScript!");
const greet = (name: string): string => {
  return `Hi, ${name}! 今天是 ${new Date().toLocaleDateString()}`;
};
export { greet };
// 这是一段只有开发者能看到的注释
// 生产环境永远不应该泄露

在这份源代码中:

它会在 console 中打印一行文字
定义一个 greet 函数 — 接收一个名字,返回一句问候语,格式是 Hi, 张三! 今天是 2026/4/24,日期是动态获取的。
然后导出这个函数,让其他文件可以引入使用。

最后两行注释是故意写的演示道具:

// 这是一段只有开发者能看到的注释
// 生产环境永远不应该泄露

用来表示这种“只有开发者才能看到”的内容,最终会原封不动地出现在 .map 文件的 sourcesContent 字段里。

package.json 中的构建配置

package.json 是整个 Node.js 项目的“身份证 + 配置中心”,决定了这个项目是什么、怎么构建、发布什么。

在文件中,scripts 定义了一组可以用 npm run <名字> 来执行的快捷命令。

比如我们的 package.json 文件的 scripts 中有:

"build": "esbuild hello.ts --bundle --outfile=dist/hello.min.js --sourcemap --minify"

当执行 npm run build 时,它会调用 esbuild 把 hello.ts 打包成 dist/hello.min.js,同时生成 .map 文件。

它本质上就是命令别名,把一长串命令藏在一个短名字后面,方便执行。

在这里,一个很关键的参数就是:

–sourcemap 它告诉 esbuild 在构建时生成一个 .map 文件,这个文件的作用是建立压缩后代码和原始代码之间的映射关系。

构建后的压缩代码示例

具体来说,构建后的 hello.min.js 是这样的:

"use strict";(()=>{console.log("Hello, World from TypeScript!");var t=e=>`Hi, ${e}! \u4ECA\u5929\u662F ${new Date().toLocaleDateString()}`;})();
//# sourceMappingURL=hello.min.js.map

变量名被缩短、代码压成一行,出了 bug 根本不知道对应原始代码的哪一行。

但是,最后一行有一个注释:

//# sourceMappingURL=hello.min.js.map

这行注释告诉浏览器和调试工具:“如果你想看原始代码,去读这个 .map 文件。”

这样,当遇到问题,浏览器 DevTools 或调试器就能自动还原代码,让你看到的是原始的 hello.ts,而不是压缩后的乱码。

.map 文件的内容揭秘

我们打开 map 文件看看 dist/hello.min.js.map

{
  "version": 3,
  "sources": ["../hello.ts"],
  "sourcesContent": [
    "// hello.ts\r\nconsole.log(\"Hello, World from TypeScript!\");\r\n\r\nconst greet = (name: string): string => {\r\n return `Hi, ${name}! 今天是 ${new Date().toLocaleDateString()}`;\r\n};\r\n\r\nexport { greet };\r\n\r\n// 这是一段只有开发者能看到的注释\r\n// 生产环境永远不应该泄露"
  ],
  "mappings": "mBACA,QAAQ,IAAI,...",
  "names": ["greet", "name"]
}

看到 sourcesContent 字段了吗?

原始的 hello.ts 源码,一字不差,完整地嵌在这里。

包括那两行注释:

  • “这是一段只有开发者能看到的注释”
  • “生产环境永远不应该泄露”

这就是 source map 的设计,这个设计在开发环境非常有用。但如果这个文件被发布出去,就是另一回事了。

发布配置中的关键失误

回到 package.json,看第二个关键点:

"files": [
  "dist"
]

files 字段控制 npm publish 时打包哪些文件。

这里写的是 "dist",意思是:把整个 dist 目录都打包进去。

dist 目录里有什么?

dist/hello.min.js ✅ 这个文件应该发布
dist/hello.d.ts ✅ 这个文件应该发布
dist/hello.min.js.map ❌ 但这个文件不应该发布,却没有被排除
dist/hello.js.map ❌ 这个文件也是不应该发布,却没有被排除

就这样,.map 文件跟着 npm 包一起发出去了。

任何人运行 npm install demo-map,解压 tgz 包,打开 dist/hello.min.js.map,就能看到完整的 TypeScript 源码。

Claude Code 泄露的真正原因

这次 Claude Code 犯的是完全一样的错误,只是规模大了几个数量级。

他们用 Bun 打包,Bun 默认开启 sourcemap,sourcesContent 默认嵌入全文。

他们发布包时没有在 files.npmignore 里排除 *.map。这样 Claude Code 的源代码就随着版本更新,安静地发布到了 npm。

总结与警示

其实 Claude Code 源码泄露的事不复杂,但又很容易被忽略。

而且这种疏忽,在每一个用 TypeScript 写 npm 包的项目里都可能发生。


更多问题探讨,请关注公众号:程序员角