4月1号不是愚人节
今天(2026.3.31)发生了一件大事:Anthropic 的 Claude Code 完整源码泄露了。http://zhuanlan.zhihu.com/p/2022389695955346888
起因挺离谱的。有人发现 npm 上 @anthropic-ai/claude-code 包里残留了 .map 文件,里面有个指向 Anthropic R2 存储桶的下载链接,没鉴权,直接就能下。zip 一解压,1903 个文件,51 万行 TypeScript,全部摊在眼前。
作为一个 Claude Code 的重度用户(也是 AI Agent 工程师),看到消息立刻 clone 下来通读了一遍。网上已经有很多文章在复述目录结构和技术栈了,这篇不聊那些。我只聊读完之后让我觉得”这个有意思”的东西。
先说技术栈
Bun + TypeScript + React + Ink。 TUI 层用 React 不算新鲜,Ink 2017 年就有了,Gatsby CLI、Prisma CLI 都在用。不过 Claude Code 的场景比一般 CLI 复杂不少:多个 Agent 并行、流式输出、工具执行中用户随时中断、权限弹窗。状态管理的复杂度到了这个级别,用 React 确实比手搓合理。
Agentic Loop:一个 while(true) 撑起整个 Agent
整个 Claude Code 最核心的文件是 src/query.ts,1729 行。注意不是 QueryEngine.ts(那个是外层的会话管理),真正的”大脑”在 query.ts 里,一个 while(true) 循环。
async function* queryLoop(params) {
let state = { messages, toolUseContext, turnCount: 1, ... }
while (true) {
// 1) 一堆预处理:裁历史、压缩上下文、预取 memory 和 skills
// 2) 调 Claude API(流式)
// 3) 一边收流一边看有没有 tool_use block
// 4) 有的话 → 检查权限 → 执行 → 结果塞回 messages → 回到 while
// 5) 没有工具调用 → 退出
}
}
看着简单,但魔鬼全在细节里。
上下文管理:不是一刀切,是四把手术刀
用过 Claude Code 的人都知道,长对话到后面它会自动”压缩”。我之前以为就是把早期对话摘要一下,读了源码才发现,它其实有四种不同粒度的压缩机制在同时工作:
- HISTORY_SNIP:最精细的一层,直接把某些消息删掉,不做任何摘要。比如一个工具返回了 500 行搜索结果,模型只用了其中 3 行。剩下 497 行就是纯噪声,留着浪费 token,摘要它也是浪费 token,直接删最划算。
- Microcompact:利用 API 层的
cache_deleted_input_tokens能力,在缓存层面做编辑。它不改消息内容,而是告诉 API”这些 token 你缓存里有但别用了”。相当于在不动消息的情况下把 token 数降下来。 - CONTEXT_COLLAPSE:把旧的对话轮次”归档”成摘要,维护一个类似 git log 的结构,每次新查询时重放。和 autocompact 的区别是它保留了结构,哪一轮做了什么事、结论是什么都还在,不是一坨糊在一起的摘要。
- Autocompact:最后的兜底,调一次模型把整个历史压缩成一段话。
四种机制按顺序依次执行,前面能搞定就不触发后面。所以大部分时候 autocompact 根本不需要跑。
读完这段我有个感触:做 Agent 的上下文管理不能只有一种策略。工具的中间输出可能几轮之后就没用了,但用户描述的需求背景可能整个会话都要保留。信息的”保质期”不一样,处理方式也该不一样。
流式工具并行:模型还在说话就开始干活了
一般 Agent 的实现是:等模型说完 → 看有没有工具调用 → 执行 → 结果返回 → 下一轮。中间有一段明显的等待。
Claude Code 不等。
// StreamingToolExecutor.ts
export class StreamingToolExecutor {
// 模型流式吐出一个 tool_use block,立刻开始执行
addTool(block: ToolUseBlock, message: AssistantMessage): void { ... }
// 并发安全的工具可以同时跑,写操作独占
// 结果按接收顺序排队,保证输出确定性
async *getRemainingResults(): AsyncGenerator<MessageUpdate> { ... }
}
模型还在流式输出后面的内容,前面的工具就已经在跑了。每个工具有个 isConcurrencySafe 标记:读文件、grep 这种只读操作可以并行跑,写文件、bash 这种需要独占。结果按接收顺序缓冲,不会乱序。
用过 Claude Code 的人应该有感觉,它的工具执行响应比较快,原因之一就是这个。工具执行的延迟被藏在了模型推理的时间里,用户几乎感知不到。
撞到输出上限也不认输
const MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3
模型输出撞到 max_output_tokens?循环不报错,”扣留”错误消息,悄悄重试,最多 3 次。对用户完全无感。
这段代码上面有段注释,模仿中世纪巫师口吻写的:
Heed these rules well, young wizard. For they are the rules of thinking, and the rules of thinking are the rules of the universe. If ye does not heed these rules, ye will be punished with an entire day of debugging and hair pulling.
“好好记住这些规则,年轻的巫师。不然你就等着花一整天调试和薅头发吧。”
维护这块代码的人显然被坑过很多次。
工具系统:40 多个工具,零继承
做过 Agent 框架的人可能都习惯写一个 BaseTool 基类然后继承。Claude Code 完全没有继承,40 多个工具全是纯函数式的 buildTool() 工厂函数:
type ToolDef<T> = {
name: string
description: string
inputSchema: ZodSchema<T> // Zod v4 做校验 + 自动生成 JSON Schema
call(input: T, ctx: ToolUseContext): AsyncGenerator<...>
isReadOnly(): boolean
getPermissions(): ToolPermission[]
renderToolUse?(input: T): ReactNode // 直接渲染到终端
getToolUseSummary?(input, result): string // 压缩上下文时的摘要
}
每个工具完全自包含:schema、权限、执行逻辑、UI 渲染、压缩摘要,全在一个文件里。没有全局注册表,每个 session 动态组装工具池,静态工具、MCP 工具、Agent 定义的工具混在一起用。
其中最复杂的是 BashTool,单个文件 1143 行。它做的远不止 exec(command):
- 自动解析命令分类成 search/read/write,用于权限匹配
- macOS 上走 sandbox-exec 沙箱,Linux 走 seccomp
- 超过 15 秒的阻塞命令自动转后台
- 大输出存磁盘,只给模型一个文件路径引用
- 内置 sed 命令解析器,检测到
sed -i时 UI 从 “Bash” 变成文件编辑样式 - 复合命令(
ls && git push)会被拆开逐段判定安全性
一个 BashTool 的复杂度就顶得上很多小型 Agent 框架的全部了。
Feature Flag:我见过最干净的功能门控
编译时:代码物理消失
import { feature } from 'bun:bundle'
const voiceModule = feature('VOICE_MODE')
? require('./voice/index.js')
: null
feature() 是 Bun 的编译时宏。构建时被替换成 true 或 false,false 的分支直接被删除。不是”运行时不执行”,是从二进制文件里物理消失,连字符串字面量都不剩。
为什么?因为安全研究员会反编译你的二进制找隐藏功能。运行时 flag 再怎么关,字符串还在那里。编译时 DCE 才是真的”不存在”。
讽刺的是,他们在二进制层面做了这么多防护,最后被一个忘删的 .map 文件全部端掉了。
我搜到了二十多个编译时 flag,每一个都对应一个未发布的功能:VOICE_MODE、BRIDGE_MODE、DAEMON、KAIROS、COORDINATOR_MODE、PROACTIVE、ABLATION_BASELINE、CONTEXT_COLLAPSE、CHICAGO_MCP……
运行时:GrowthBook A/B 测试
const enabled = checkStatsigFeatureGate_CACHED_MAY_BE_STALE(
'tengu_streaming_tool_execution2'
)
用于灰度发布和紧急 kill switch。所有 gate 名称都以 tengu_ 开头,tengu(天狗)是 Claude Code 项目的内部代号。从磁盘缓存读取,接受脏读,不阻塞启动。
消融实验:用科学方法做产品
这个发现让我挺意外的。有个 flag 叫 ABLATION_BASELINE,启用后会一次性关掉思考模式、上下文压缩、自动记忆、后台任务:
if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) {
for (const k of [
'CLAUDE_CODE_DISABLE_THINKING',
'DISABLE_COMPACT',
'DISABLE_AUTO_COMPACT',
'CLAUDE_CODE_DISABLE_AUTO_MEMORY',
'CLAUDE_CODE_DISABLE_BACKGROUND_TASKS',
]) {
process.env[k] ??= '1';
}
}
做过 ML 研究的人都熟悉消融实验:逐个关掉组件看对最终效果的影响。但把这个方法论搬到产品工程上,在工业代码里我还是第一次见。
这意味着 Anthropic 每上线一个新功能(thinking、compact、memory……),都可以跑一组对照实验量化它的价值。不是”感觉有用就上”,是”数据证明有用才上”。
隐藏功能:源码里还没发布的东西
编译时 flag 门控住的那些功能,虽然在公开版二进制里看不到,但源码全暴露了。
Voice Mode(代号 Amber Quartz)
src/voice/ 目录确认了语音模式的存在:
- 只支持 Claude.ai OAuth 认证(API key、Bedrock、Vertex 都不行)
- 走专门的
voice_stream端点 - 有紧急 kill switch:
tengu_amber_quartz_disabled - 从注释看已经开发完成,只是还没公开
Bridge Mode:把你的电脑变成 Claude 的远程终端
src/bridge/ 有 31 个文件,实现了一个完整的远程控制系统。运行 claude remote-control 之后,你的本地环境就变成一个可以被 claude.ai 远程操控的”桥接环境”。
最多支持 32 个并发会话,JWT 认证加可信设备机制,企业管理员可以通过策略禁用。这应该是为了让 claude.ai 网页版能直接操作你本地的开发环境,不用再手动复制粘贴代码了。
Buddy:终端里的电子宠物
这个绝对是全源码里最出人意料的发现。
Claude Code 内置了一个完整的虚拟宠物系统,而且没有用 feature flag 门控,已经在每个用户的二进制里了:
// 18 种宠物
export const SPECIES = [
duck, goose, blob, cat, dragon, octopus, owl, penguin,
turtle, snail, ghost, axolotl, capybara, cactus, robot,
rabbit, mushroom, chonk
] as const
// 5 级稀有度
export const RARITY_WEIGHTS = {
common: 60, uncommon: 25, rare: 10, epic: 4, legendary: 1,
}
// RPG 式属性
export const STAT_NAMES = ['DEBUGGING','PATIENCE','CHAOS','WISDOM','SNARK'] as const
18 种宠物,5 级稀有度(传说概率 1%),1% 闪光变体。还有帽子系统(皇冠、礼帽、螺旋桨帽、光环、巫师帽、豆豆帽、头顶小鸭子)和不同的眼睛样式。宠物属性用 Mulberry32 伪随机数生成器从用户 ID 确定性计算,每个用户绑定一只,不能刷。
在一个 51 万行的严肃工程项目里发现一套完整的抽卡养宠系统,还是挺有意思的。
宠物名字里藏了一个秘密
但比宠物本身更有意思的是它物种名的编码方式:
const c = String.fromCharCode
export const duck = c(0x64,0x75,0x63,0x6b) as 'duck'
export const goose = c(0x67,0x6f,0x6f,0x73,0x65) as 'goose'
export const capybara = c(0x63,0x61,0x70,0x79,0x62,0x61,0x72,0x61) as 'capybara'
18 个物种名全部用 hex 编码,一个都没用明文。注释写着:”One species name collides with a model-codename canary in excluded-strings.txt.”
也就是说,其中一个宠物名恰好是 Anthropic 某个未公开模型的内部代号。构建系统会 grep 二进制产物里有没有黑名单里的字符串,所以必须 hex 编码绕过检测。
到底是哪一个?这就要结合另一次泄露来看了。
模型代号:两次泄露拼出完整拼图
3 月 28 日,也就是这次 npm 泄露的三天前,Anthropic 还出了另一件事:CMS 数据库权限没关,被 Fortune 记者翻出了近 3000 份内部文件。其中提到一个从未公开的模型叫 Claude Mythos,内部代号 Capybara(水豚),定位在 Opus 之上的全新层级。
而在 Claude Code 源码的 prompts.ts 里,我找到了大量 @[MODEL LAUNCH] 注释(给新模型发布准备的 TODO checklist),里面反复出现这个名字:
// @[MODEL LAUNCH]: Update comment writing for Capybara
// — remove or soften once the model stops over-commenting by default
// @[MODEL LAUNCH]: False-claims mitigation for Capybara v8
// (29-30% FC rate vs v4's 16.7%)
这些 TODO 还没完成,说明 Capybara 还没公开发布。但是 main.tsx 里已经有 capybara-fast、capybara-v2-fast[1m] 这些模型别名,说明 Anthropic 内部员工已经在日常使用了。
拼到一起就很清楚了:Capybara 是比 Opus 更强的下一代旗舰 Mythos 的内部代号。Opus 4.6 在代码里就叫 claude-opus-4-6,没有动物名。宠物系统里撞车的就是这只水豚。
顺带泄露了一个内部数据:Capybara v8 的虚假声明率 29-30%,比上一版 v4 的 16.7% 翻了快一倍。Anthropic 没有回退版本,而是在 prompt 层加指令做修补,内部员工先当小白鼠验证效果。
源码里还出现了另一个模型代号:
// @[MODEL LAUNCH]: Remove this section when we launch numbat.
Numbat(袋食蚁兽),又一个待发布的模型。和 Capybara 的关系目前还不清楚。
此外,Claude Code 项目本身的代号是 Tengu(天狗),不知道月亮指代的是什么😂
Skill 系统和多 Agent 协调
简单提两个设计,因为思路值得参考。
Skills 就是 Markdown 文件。 .claude/skills/ 目录下放 .md 文件,YAML frontmatter 里写描述、触发条件、允许的工具、用哪个模型。Claude Code 读文件时如果发现目录下有 skills,会自动加载,不用显式注册。”约定优于配置”,很 Rails。
多 Agent 协调器意外地简单。Coordinator 模式下主 Agent 只有三个工具:生成 worker、给 worker 发消息、停止 worker。Worker 拿不到 TeamCreate 和 SendMessage,防止套娃。后端支持 tmux pane、in-process、remote 三种方式。
几个容易被忽略的工程细节
隐私保护写进了类型名:埋点数据的类型叫 AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS。用类型名本身提醒开发者”你确认过这不是代码或文件路径了吗”。简单,但有效。
投机执行:AppState 里有 speculationState,追踪每一轮的结束方式(bash / 文件编辑 / 正常结束 / 权限拒绝),用来预判下一步操作并提前执行。这解释了为什么 Claude Code 有时候”想”完就瞬间开始干活。
冷启动优化:--version 路径做到了零 import,直接读编译时内联的版本号,一个模块都不加载就退出。其他子命令走独立的 import() 路径。只有最终进主循环才加载完整的 React 应用。一个 800KB 的 React app,你不用的功能就不加载,启动时间自然快。
泄露原因
npm 发布时忘删 .map 文件,map 里引用了 R2 上的源码 zip,那个 URL 没有访问控制。就这么简单。
所以给所有发 npm 包的人提个醒:
package.json的files字段要白名单制,只包含你想发布的东西- CI 里加一步检查发布产物有没有
.map文件 - 源码归档 URL 要有鉴权,别裸挂在 CDN 上
- 构建产物和源码的访问控制应该独立管理
他们在二进制里做了那么多防泄漏设计(编译时 DCE、物种名 hex 编码、excluded-strings 黑名单),最后被一个忘删的 map 文件全部端掉了。安全这件事,100 个环节做对 99 个,漏一个就等于没做。
写在最后
读完 51 万行代码,我最大的感受不是某个具体技术多牛,而是这个团队在用做研究的方法做工程。
消融实验基础设施、双层 feature flag、四种粒度的上下文管理、流式工具并行,每一个都不是拍脑袋加的,背后大概率有数据在支撑。这种”每个功能都有量化验证”的工程文化,比任何单点技术都值得学。
当然,宠物系统除外。那个纯粹是因为好玩。
源码我还在继续读,后面可能会写一篇深入某个模块的分析(BashTool 1143 行的沙箱和命令安全机制,或者 Coordinator 模式的多 Agent 编排细节,大家想看哪个?)。