1. 一次打包疏忽,把 TypeScript 源码送到了每个人手里
Claude Code 源码泄露这件事,本质上不是什么高深的安全攻防,而是一次 npm 发包时的低级失误——发布包里忘了剔除 Source Map 文件。Source Map 是什么?你可以把它理解成一张“翻译对照表”:打包工具把 TypeScript 编译、压缩、混淆成浏览器或 Node 能跑的 JavaScript 之后,这张表记录了每一段压缩代码对应原始文件的哪一行哪一列。它本来是给开发者调试用的,一旦跟着 npm 包一起发出去,任何人拿到它,都能用现成工具把压缩后的代码“反向翻译”回接近原始的 TypeScript 结构。
这次泄露的包里包含近 2000 个核心源文件,模型权重和推理逻辑不在其中,但 CLI 工具的工程架构、Agent 调度逻辑、工具调用链的设计几乎完整暴露。对普通用户来说这可能只是条新闻,但对前端和 Node 开发者来说,这是一次难得的、可以亲手复现的还原链路。下面我就按“拉包 → 定位 .map → 还原目录 → 验证完整性”的顺序,把整条链路拆开讲清楚,命令都可以直接复制运行。
2. 复现前的前置准备:Node 环境与 TaoToken 接入
要复现还原过程,你本地需要一套能跑 Node 脚本的环境。Node 18 以上即可,npm 自带。如果你后续想对照还原出来的源码去理解 Agent 调度逻辑,或者想自己跑一些模型调用来验证工具链行为,可以顺手把 TaoToken 的接入配好——它提供统一的 API 入口,兼容常见的模型调用格式,配置一次就能在脚本里直接请求。
TaoToken 的 API 地址是 https://taotoken.net/api ,控制台在 https://taotoken.net/console ,API Keys 管理页在 https://taotoken.net/api-keys 。如果你打算长期做编码类实验或 Agent 相关开发,可以了解下 Coding Plan:https://taotoken.net/coding-plan 。想先验证模型对话是否通,用模型对话页:https://taotoken.net/models 。接入文档在 https://taotoken.net/doc ,Claude Code 相关说明在 https://taotoken.net/claude-code 。
这部分不是还原源码的必需步骤,但如果你还原之后想跑通工具调用链、验证某些请求行为,有一个稳定的 API 入口会省很多事。配置方式很简单,在项目根目录建一个.env文件:
# .env TAOTOKEN_API_KEY=你的key TAOTOKEN_BASE_URL=https://taotoken.net/api然后在 Node 脚本里读取:
// config.js import 'dotenv/config'; export const apiKey = process.env.TAOTOKEN_API_KEY; export const baseURL = process.env.TAOTOKEN_BASE_URL; if (!apiKey) { throw new Error('缺少 TAOTOKEN_API_KEY,请检查 .env 文件'); }这样后续任何需要发请求验证的脚本都能直接复用这份配置。
3. 从 npm 拉包到定位 .map 文件
第一步是把目标版本的包拉到本地。不要直接npm install到项目里,那样会污染依赖树,用npm pack下载 tarball 更干净:
mkdir claude-code-restore && cd claude-code-restore npm pack @anthropic-ai/claude-code --pack-destination ./pkg执行完你会看到pkg/目录下多了一个.tgz文件。解压它:
cd pkg tar -xzf anthropic-ai-claude-code-*.tgz cd package ls -la这时候重点来了:找.map文件。Source Map 通常和编译产物放在一起,命名规律是xxx.js.map。用 find 快速定位:
find . -name "*.map" -type f | head -50如果输出里出现大量.map文件,说明这个包确实把 Source Map 一起发布了。你可以进一步统计数量:
find . -name "*.map" -type f | wc -l我实测下来,这类失误包里的.map文件往往和.js文件一一对应。随便打开一个看看结构:
cat ./dist/cli.js.map | head -c 500你会看到类似这样的 JSON 结构:
{ "version": 3, "file": "cli.js", "sources": ["../src/cli.ts", "../src/agent/dispatcher.ts"], "sourcesContent": ["import ...", "export ..."], "names": [], "mappings": "AAAA,IAAM..." }关键字段是sources和sourcesContent。sources记录了原始文件路径,sourcesContent如果存在,就直接内嵌了原始源码文本——这意味着你甚至不需要做复杂的映射计算,直接把sourcesContent抽出来就是源码。
4. 用 source-map 工具还原完整目录结构
如果sourcesContent存在,还原非常简单。写一个 Node 脚本批量处理:
// restore.js import fs from 'fs'; import path from 'path'; import { glob } from 'glob'; const MAP_DIR = './package'; const OUT_DIR = './restored'; async function restoreFromMap(mapFile) { const raw = fs.readFileSync(mapFile, 'utf-8'); const map = JSON.parse(raw); if (!map.sourcesContent) { console.warn(`跳过(无 sourcesContent): ${mapFile}`); return; } map.sources.forEach((src, i) => { const content = map.sourcesContent[i]; if (content == null) return; // 去掉 ../ 前缀,避免路径逃逸 const safeRel = src.replace(/^(\.\.\/)+/, ''); const outPath = path.join(OUT_DIR, safeRel); fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, content, 'utf-8'); }); console.log(`已还原: ${mapFile} -> ${map.sources.length} 个文件`); } const maps = await glob(`${MAP_DIR}/**/*.map`); console.log(`共发现 ${maps.length} 个 map 文件`); for (const m of maps) { await restoreFromMap(m); }运行:
npm init -y npm install glob node restore.js跑完之后restored/目录下就会出现还原出来的 TypeScript 文件树。你可以用tree或find看结构:
find restored -name "*.ts" | head -30 find restored -name "*.ts" | wc -l如果sourcesContent不存在,那就需要用source-map库做真正的映射还原,流程会复杂一些,需要读取压缩后的 JS 和 mappings 字段逐段反解。但这次泄露的包里sourcesContent基本都在,所以上面这个脚本就够用了。
5. 验证还原结果是否完整
还原完不能只看文件数量,得验证内容是否完整、是否可读。几个检查动作:
第一,检查文件是否为空或截断:
find restored -name "*.ts" -size -10c如果输出为空,说明没有过小的异常文件。
第二,抽查关键文件的开头,看是否是合法 TypeScript:
head -20 restored/src/cli.ts正常应该看到 import 语句、类型定义、函数声明,而不是乱码或压缩后的单行代码。
第三,统计还原文件数和 map 里声明的 sources 总数是否一致:
// verify.js import fs from 'fs'; import { glob } from 'glob'; const maps = await glob('./package/**/*.map'); let declared = 0; let restored = 0; for (const m of maps) { const map = JSON.parse(fs.readFileSync(m, 'utf-8')); declared += (map.sources || []).length; } const files = await glob('./restored/**/*.ts'); restored = files.length; console.log(`map 声明源文件数: ${declared}`); console.log(`实际还原文件数: ${restored}`); console.log(declared === restored ? '数量一致' : '存在差异,需排查');第四,用 TypeScript 编译器做语法检查,看还原出来的代码是否能通过解析:
npx tsc --noEmit --allowJs false --skipLibCheck restored/src/cli.ts如果报大量语法错误,说明还原过程中有内容损坏;如果只是类型缺失报错,那属于正常,因为依赖没装。
6. 常见报错与排查
报错一:Cannot find module 'glob'说明依赖没装。执行npm install glob即可。如果你用的是 Node 20 以下,glob 的 ESM 导入可能需要加--experimental-vm-modules,或者改用glob的 CommonJS 版本glob@8。
报错二:SyntaxError: Unexpected token 'export'你的package.json里没有声明"type": "module"。在npm init -y之后手动加上:
{ "type": "module" }报错三:还原出来的文件路径带../导致写到项目外面脚本里已经做了replace(/^(\.\.\/)+/, '')处理,但如果你自己改脚本,记得保留这一步。否则sources里的../../src/xxx.ts会写到上层目录,污染你的工作区。
报错四:sourcesContent为 null部分 map 文件可能只存了 mappings 没存内容。这时候要么跳过,要么用source-map库的SourceMapConsumer逐行反解:
import { SourceMapConsumer } from 'source-map'; const consumer = await new SourceMapConsumer(map); consumer.eachMapping(m => { console.log(m.source, m.originalLine, m.originalColumn); });报错五:还原后文件数远少于预期检查是不是只处理了顶层 map。用glob('./package/**/*.map')确保递归匹配所有子目录。另外有些包会把 map 放在dist/之外的目录,find 的时候别限定路径。
7. 还原之后能拿这份源码做什么
还原只是第一步。拿到可读的 TypeScript 源码之后,你可以对照着看它的 Agent 调度是怎么组织的、工具调用链是怎么串起来的、上下文是怎么裁剪的。这些工程细节在文档里通常不会写,但在源码里一目了然。
如果你想在本地跑一些验证脚本,比如模拟一次工具调用请求,可以用前面配好的 TaoToken 接入:
// test-call.js import 'dotenv/config'; const res = await fetch(`${process.env.TAOTOKEN_BASE_URL}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.TAOTOKEN_API_KEY, 'anthropic-version': '2023-06-01' }, body: JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 256, messages: [{ role: 'user', content: '用一句话说明 Source Map 的作用' }] }) }); const data = await res.json(); console.log(data.content?.[0]?.text ?? data);跑通之后,你就能把还原出来的调度逻辑和自己的请求脚本对照起来看。想先验证模型对话是否正常,可以直接用模型对话页:https://taotoken.net/models 。需要管理 key 就去 API Keys 页:https://taotoken.net/api-keys 。接入细节看文档:https://taotoken.net/doc 。长期做编码和 Agent 实验的话,Coding Plan 会更合适:https://taotoken.net/coding-plan 。
整条链路的核心其实就三件事:npm pack 拉包、find 定位 map、脚本抽 sourcesContent。真正花时间的是验证环节——确认还原出来的文件不是空壳、不是截断、路径没写错。把这几个检查动作跑一遍,你手里就有一份可读的 TypeScript 源码树了。