一场意外的代码泄露,让我们第一次看清了这头野兽的全貌。
事情的起因
2026年3月31日,安全研究者 Chaofan Shou 在推上发了一条不起眼的帖子:
"Anthropic 发布到 npm 的 Claude Code 包里,source map 没有被剥离。"
翻译成人话:Claude Code 的完整 TypeScript 源码,51.2万行,1903个文件,就这样裸奔在公网上。
这种事放在任何一家公司都是一场小型事故。但对我们这些对 AI 工具感兴趣的人来说,这是一份意外的圣诞礼物。
我当然不可能在几小时内把51万行代码读完。所以我带着三个问题去翻这份代码:
- Claude Code 和其他 AI 编程工具,到底有什么本质区别?
- 为什么它写代码的"手感"就是比别人好?
- 51万行代码里,到底藏着什么不为人知的东西?
读完之后,我的第一反应是——
这不是一个 AI 编程助手。这是一个操作系统。
先讲一个故事:如果你要雇一个远程程序员
在进入源码之前,我想先讲清楚一件事:为什么市面上的 AI 编程工具"感觉"不一样?
想象你雇了一个远程程序员,要给他远程访问你电脑的权限。不同公司的方案,代表了完全不同的哲学:
Cursor 的方案:让程序员坐你旁边,每次他要执行命令,你都得看一眼、点个"允许"。简单、透明,但你得一直盯着,稍微走开就不行。
GitHub Copilot Agent 的方案:给他一台全新的虚拟机,让他在里面随便折腾,搞完了把代码提交上来,你审核后再合并。隔离得很干净,但他永远用不了你本地的环境、你的配置、你的 API key。
Claude Code 的方案:直接让他用你的电脑——但给他配了一套极其精密的安检系统。他能做什么、不能做什么、哪些操作需要你点头、哪些可以自己来、甚至他想执行 rm -rf 都要过9道关卡。
这就是三种完全不同的安全哲学。
为什么 Anthropic 选了最难的那条路?因为只有这样,AI 才能真正用你的终端、你的环境、你的配置来干活。这才是"替你写代码",而不是"在一个无菌室里给你写一段代码然后复制粘贴过来"。
代价是什么?51万行代码。
你以为的 Claude Code,和真实的 Claude Code
大多数人以为 AI 编程工具的工作原理大概是这样的:
用户输入 → 调用 LLM API → 返回结果 → 显示给用户
Claude Code 实际上是这样的:
用户输入
→ 动态组装 7 层系统提示词
→ 注入 Git 状态、项目约定、历史记忆
→ 42 个工具各自附带独立使用手册
→ LLM 决定调用哪个工具
→ 9 层安全审查(AST解析、ML分类器、沙箱检查...)
→ 权限竞争解析(本地键盘/IDE/Hook/AI分类器 同时竞争)
→ 200ms 防误触延迟
→ 执行工具
→ 结果流式返回
→ 上下文接近极限?→ 三层压缩启动
→ 需要并行?→ 生成子 Agent 蜂群
→ 循环直到任务完成
这不是一个"聊天机器人加几个插件",这是一条工业级的生产流水线。
接下来,我们逐层拆开。
第一层:提示词不是"写"出来的,是"编译"出来的
很多人学 AI 开发,第一件事就是研究"怎么写好 prompt"。但如果你打开 Claude Code 的 src/constants/prompts.ts,你会发现这个问题在 Anthropic 这里根本不存在——因为他们压根不是"写"提示词的。
他们是在组装提示词。
export async function getSystemPrompt(
tools: Tools,
model: string,
additionalWorkingDirectories?: string[],
mcpClients?: MCPServerConnection[],
): Promise<string[]> {
return [
// --- 静态部分(可缓存)---
getSimpleIntroSection(outputStyleConfig),
getSimpleSystemSection(),
getSimpleDoingTasksSection(),
getActionsSection(),
getUsingYourToolsSection(enabledTools),
getSimpleToneAndStyleSection(),
getOutputEfficiencySection(),
// === 缓存边界 ===
...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []),
// --- 动态部分(每次不同)---
...resolvedDynamicSections,
].filter(s => s !== null)
}
注意到 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 这个东西了吗?
这是一条缓存分界线。分界线上面的内容是静态的,可以被 Claude API 缓存,不重复计费;分界线下面是动态的——你当前的 Git 分支、你项目里的 CLAUDE.md 配置、你之前告诉它的习惯偏好……每次对话都不一样。
这个设计的高明之处在于:
- 省钱:静态部分走缓存,不重复计费
- 提速:缓存命中直接跳过这些 token 的处理
- 灵活:动态部分让每次对话都能感知当前环境
Anthropic 把提示词当成了编译器的输出来管理。静态部分是"编译后的二进制",动态部分是"运行时参数"。这是工程化的提示词管理,不是手工艺。
每个工具都有自己的"员工手册"
更让我惊讶的是:每个工具目录下都有一个 prompt.ts 文件——这是专门写给 LLM 看的使用规范。
看看 BashTool 的(光这一个文件就有约 370 行):
Git Safety Protocol:
- NEVER update the git config
- NEVER run destructive git commands (push --force, reset --hard,
checkout .) unless the user explicitly requests
- NEVER skip hooks (--no-verify) unless the user explicitly requests
- CRITICAL: Always create NEW commits rather than amending
这不是写给人看的文档,是写给 AI 看的行为准则。每次 Claude Code 启动时,这些规则都会被注入到系统提示词里。
这就是为什么 Claude Code 不会擅自 git push --force,而某些工具会——不是模型更聪明,是提示词里已经把规矩讲清楚了。
内部版本和你用的不一样
代码里大量出现这种分支:
const minimalUniquenessHint =
process.env.USER_TYPE === 'ant'
? '\n- Use the smallest old_string that's clearly unique'
: ''
ant 就是 Anthropic 内部员工。他们的版本有更详细的代码风格指引("不写注释,除非 WHY 不明显")、更激进的输出策略("倒金字塔写作法"),以及一些仍在 A/B 测试的实验功能,比如 Verification Agent、Explore & Plan Agent。
这说明什么?Anthropic 自己就是 Claude Code 最重度的用户。他们在用自己的产品来开发自己的产品,然后把踩过的坑直接写进提示词。
第二层:42个工具,但你只看到了冰山一角
打开 src/tools.ts,可以看到工具注册中心:
export function getAllBaseTools(): Tools {
return [
AgentTool,
BashTool,
FileReadTool, FileEditTool, FileWriteTool,
GlobTool, GrepTool,
WebFetchTool, WebSearchTool,
TodoWriteTool, NotebookEditTool,
...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
// ... 还有更多条件加载的工具
]
}
42个工具,但大部分你从来没有直接见过。因为很多工具是延迟加载的——只有当 LLM 真正需要时,才通过 ToolSearchTool 按需注入。
为什么要这么麻烦?
因为每多一个工具,系统提示词就要多一段描述,token 就多花一份钱。你只是想让它帮你改一行代码,它没必要加载"定时任务调度器"和"团队协作管理器"。
还有一个彩蛋:
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
return filterToolsByDenyRules(simpleTools, permissionContext)
}
设置 CLAUDE_CODE_SIMPLE=true,Claude Code 就只剩三个工具:Bash、读文件、改文件。这是给极简主义者留的后门,也是给网络条件差或者 token 预算紧张的用户留的逃生通道。
"fail-closed":宁可过度保守,也不漏掉风险
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?) => false, // 默认:不并发安全
isReadOnly: (_input?) => false, // 默认:会写入
isDestructive: (_input?) => false,
}
注意那些默认值——isConcurrencySafe 默认 false,isReadOnly 默认 false。
这叫 fail-closed 设计:如果一个工具的开发者忘了声明安全属性,系统会假设它是"不安全的、会写入的"。宁可过度保守,也不漏掉一个风险。
这和很多公司的做法截然相反——很多系统的默认值是"允许",出了问题再补救。Anthropic 从一开始就选了"拒绝"作为默认值。
"先读后改"的铁律
function getPreReadInstruction(): string {
return '\n- You must use your `Read` tool at least once in the
conversation before editing. This tool will error if you attempt
an edit without reading the file.'
}
FileEditTool 会强制检查:你有没有用 FileReadTool 读过这个文件?没读过,直接报错,不让改。
这就是为什么 Claude Code 不会"凭空写一段代码覆盖你的文件"——它被系统强制要求先理解再修改。这个约束不是靠模型的"道德感",而是写死在工具的准入逻辑里的。
第三层:记忆系统——它是怎么"认识你"的
用过 Claude Code 一段时间的人,都会有一个感受:它好像真的认识你。
你告诉它"不要在测试里 mock 数据库",下次对话它就记住了。你告诉它"我是后端工程师,React 新手",它解释前端代码时就会用后端的类比。你的项目里有什么奇怪的约定,它一旦学到了就不会违反。
这背后是一个完整的记忆系统,而且这个系统本身也是用 AI 驱动的。
用 AI 来检索记忆
const SELECT_MEMORIES_SYSTEM_PROMPT =
`You are selecting memories that will be useful to Claude Code.
Return a list of filenames for the memories that will clearly
be useful (up to 5).
- If you are unsure if a memory will be useful, do not include it.
- If a list of recently-used tools is provided, do not select
memories that are usage reference for those tools. DO still
select memories containing warnings, gotchas, or known issues.`
Claude Code 用另一个 AI(Claude Sonnet)来决定"哪些记忆和当前对话相关"。
不是关键词匹配,不是向量数据库——而是让一个小模型快速扫描所有记忆文件的标题和描述,选出最多5个最相关的,然后把它们完整地注入到当前对话的上下文里。
策略是精确度优先于召回率——宁可漏掉一个可能有用的记忆,也不塞进一个不相关的记忆来污染上下文。
想想这意味着什么:连"该记住什么、该忘记什么"这件事,Anthropic 都没有用传统的工程方案来解决,而是直接用另一个 AI 来做判断。
KAIROS:AI 在"做梦"
这是整份源码里最让我觉得科幻的部分。
代码里有一个叫 KAIROS 的特性标志。在这个模式下,长会话中的记忆不是存储在结构化文件里,而是存在按日期追加的原始日志里。然后,有一个叫 /dream 的技能会在低活跃期(也就是"夜间")运行,把这些原始日志蒸馏成结构化的主题文件:
logs/2026/03/2026-03-30.md ← 今天的原始记录
↓ /dream 蒸馏
memory/user_preferences.md ← 结构化的用户偏好
memory/project_context.md ← 结构化的项目背景
AI 在"睡觉"的时候整理记忆。
这已经不是工程了,这是仿生学。人类的大脑在睡眠期间会把短期记忆转化为长期记忆,海马体会在 REM 睡眠中"重播"白天的经历并完成整合。KAIROS 做的是同一件事。
我不知道这个功能现在是否已经在生产环境启用,但光是"有人在认真设计这个"这件事本身,就足够令人震惊。
第四层:它不是一个 Agent,是一群
当你让 Claude Code 处理一个复杂任务时,它可能悄悄做了你完全不知道的事:
// AgentTool 的输入 schema
z.object({
description: z.string().describe('A short (3-5 word) description'),
prompt: z.string().describe('The task for the agent to perform'),
subagent_type: z.string().optional(),
model: z.enum(['sonnet', 'opus', 'haiku']).optional(),
run_in_background: z.boolean().optional(),
})
它生成了一个子 Agent——一个专门负责某个子任务的小 Claude,在后台悄悄运行。
而且子 Agent 有严格的"自我认知"注入,防止它递归地再生成更多子 Agent:
export function buildChildMessage(directive: string): string {
return `STOP. READ THIS FIRST.
You are a forked worker process. You are NOT the main agent.
RULES (non-negotiable):
1. Your system prompt says "default to forking." IGNORE IT —
that's for the parent. You ARE the fork.
Do NOT spawn sub-agents; execute directly.
2. Do NOT converse, ask questions, or suggest next steps
3. USE your tools directly: Bash, Read, Write, etc.
4. Keep your report under 500 words.
5. Your response MUST begin with "Scope:". No preamble.`
}
这段提示词翻译成大白话就是:你是工人,不是经理。别想着再招人,自己干活。
这个设计解决了多 Agent 系统里一个经典问题:递归爆炸。如果每个 Agent 都可以随意生成子 Agent,那很容易出现"Agent 生成 Agent 生成 Agent"的死循环,token 费用直接爆炸。通过在子 Agent 的提示词里硬编码"禁止分叉",Anthropic 在系统设计层面掐断了这个可能性。
协调器模式:AI 版的项目经理
在协调器模式下,Claude Code 变成一个纯粹的任务编排者,自己不动手,只分配任务:
Phase 1: Research → 3 个 worker 并行搜索代码库
Phase 2: Synthesis → 主 Agent 综合理解所有发现
Phase 3: Implementation → 2 个 worker 分别修改不同文件
Phase 4: Verification → 1 个 worker 跑测试
核心原则写在代码注释里: "Parallelism is your superpower"
只读任务(搜索、分析):全部并行跑。 写文件任务:按文件分组,串行执行(避免冲突)。
这是一个非常务实的设计——并行快,但写同一个文件会冲突,所以只在安全的地方并行。
一个省钱的小细节
为了最大化子 Agent 的缓存命中率,所有 fork 子 Agent 的工具调用结果都使用相同的占位符文本:
'Fork started — processing in background'
为什么?因为 Claude API 的 prompt cache 是基于字节级前缀匹配的。如果 10 个子 Agent 的前缀字节完全一致,那么只有第一个需要"冷启动",后面 9 个直接命中缓存。
每次调用节省几美分,听起来微不足道。但如果你每天跑几千个 Agent,一个月能省下的数字就很可观了。Anthropic 在这种犄角旮旯的地方也要抠成本,足见他们对规模化成本的重视程度。
第五层:三层压缩,让对话"永不超限"
所有 LLM 都有上下文窗口限制。对话越长,历史消息越积越多,最终一定会超出限制。大多数工具的处理方式是:超了就截断。这会导致 AI 突然"失忆",之前讨论的东西它全不知道了。
Claude Code 为此设计了三层渐进式压缩:
第一层:微压缩——外科手术式清理
export async function microcompactMessages(messages, toolUseContext, querySource) {
// 时间触发:如果上次交互已过很久,服务器缓存已冷
const timeBasedResult = maybeTimeBasedMicrocompact(messages, querySource)
if (timeBasedResult) return timeBasedResult
// 缓存编辑路径:通过 API 的缓存编辑功能直接删除旧内容
if (feature('CACHED_MICROCOMPACT')) {
return await cachedMicrocompactPath(messages, querySource)
}
}
微压缩只处理旧的工具调用结果——把"10分钟前读的那个500行文件的内容"替换成 [Old tool result content cleared]。对话的主线、你的指令、AI 的回复,全部保留。
这就像清理浏览器缓存,但只清理你不再需要的那些 tab 里的资源,而不是关掉所有 tab。
第二层:自动压缩——提前预警
当 token 消耗接近上下文窗口的 87% (窗口大小减去 13,000 的 buffer),自动压缩触发。
还有一个熔断器机制:连续 3 次压缩失败后,停止继续尝试,避免死循环。这个细节很有意思——大多数系统在设计时不会考虑"如果压缩本身失败了怎么办",但 Claude Code 考虑了。
第三层:完全压缩——AI 总结全场
当前两层都不够用时,让 AI 对整段对话生成一份摘要,然后用摘要替换掉所有历史消息。
生成摘要时有一段很严厉的前置指令:
const NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY.
Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- Tool calls will be REJECTED and will waste your only turn.`
为什么要这么凶?因为如果总结过程中 AI 又去调工具,就会产生更多 token 消耗,适得其反——你是为了省 token 来总结的,结果总结过程消耗的 token 比你省的还多,这个笑话就开不下去了。
这段提示词是在对 AI 说:"你现在只有一件事,就是总结。别想别的。"
压缩后给不同内容分配的 token 预算:
- 文件恢复:50,000 tokens
- 每个文件上限:5,000 tokens
- 技能内容:25,000 tokens
这些数字不是拍脑袋定的,是在"保留足够上下文继续工作"和"腾出足够空间接收新消息"之间反复调参的结果。
第六层:安全系统——9道关卡只为让你安全敲一行命令
这是整个代码库里最让我意外的部分:单是 BashTool 的安全相关代码,就有 18 个文件。
一个工具,18个安全文件。
这9层审查大概是这样的:
- 静态词法分析:你的命令里有没有明显危险的模式?(比如
rm -rf /) - AST 解析:把 shell 命令解析成语法树,看命令的结构是否危险
- ML 分类器:用机器学习模型判断这个命令的意图
- 沙箱预执行:在隔离环境里先跑一遍,看会发生什么
- 权限检查:这个操作是否在用户的允许范围内?
- 上下文理解:结合当前任务,这个命令合理吗?
- 破坏性评估:这个操作可逆吗?(不可逆的要额外审查)
- 竞争解析:多个权限来源(本地、IDE、Hook、AI)同时给出判断,谁说了算?
- 200ms 冷却:最终执行前强制等200毫秒,防止误操作
200ms 的延迟设计尤其有意思。它不是为了性能,是为了给人类最后一次反应的机会。在机器的时间尺度上,200ms 是永恒;在人类的时间尺度上,200ms 刚好够你看清楚将要发生什么、然后决定要不要拦截。
这是一个专门为"人机协作"设计的停顿。
读完51万行代码,我真正学到了什么
AI Agent 的90%工作量在"AI"之外
51万行代码里,真正调用 LLM API 的部分可能不到5%。
其余95%是什么?
- 安全检查(18个文件只为一个 BashTool)
- 权限系统(allow/deny/ask/passthrough 四态决策树)
- 上下文管理(三层压缩 + AI 记忆检索)
- 错误恢复(熔断器、指数退避、Transcript 持久化)
- 多 Agent 协调(蜂群编排 + 邮箱通信)
- UI 交互(140个 React 组件 + IDE Bridge)
- 性能优化(prompt cache 稳定性 + 启动时并行预取)
如果你正在做 AI Agent 产品,这才是你真正要解决的问题。不是模型够不够聪明——是你的脚手架够不够结实。
很多人做 AI 应用的思路是:找一个好模型,写几个 prompt,调一调,上线。这条路做出来的东西,永远只是个 Demo。Claude Code 的源码告诉你:要做一个真正可用、可信任的 AI 工具,你需要的是工程,不是魔法。
提示词工程是系统工程
不是写一段漂亮的 system prompt 就完事了。Claude Code 的提示词是:
- 7层动态组装,根据当前状态实时生成
- 每个工具有独立的行为规范,单独维护
- 缓存边界精确划分,静态和动态分开管理
- 内外版本有不同的指令集,持续 A/B 测试
- 工具顺序固定,保持缓存的字节级稳定性
这是工程化的提示词管理体系,不是某个人坐在那里"调 prompt"。
为失败而设计
Claude Code 的每一个外部依赖都有对应的失败策略:
- 压缩失败?熔断器生效,停止重试
- 记忆检索超时?跳过记忆,继续对话
- 子 Agent 无响应?主 Agent 收到超时信号,继续其他任务
- 文件读取失败?报错信息被格式化后回传给 LLM,让它决定下一步
没有哪个地方是"出错了就崩溃"。每一条错误路径都被提前设想过,并给出了降级处理方案。
这是做过生产系统的工程师才有的直觉:系统不能假设外部世界是正常的。
Anthropic 在造的不是工具,是基础设施
| Claude Code 的层 | 对应操作系统的概念 |
|---|---|
| 42个工具 | 系统调用 |
| 权限系统 | 用户权限管理 |
| 技能系统 | 应用商店 |
| MCP 协议 | 设备驱动 |
| Agent 蜂群 | 进程管理 |
| 上下文压缩 | 内存管理 |
| Transcript 持久化 | 文件系统 |
| KAIROS 记忆 | 睡眠整合 |
这个类比不是牵强的——它是在设计层面就有意为之的。
Anthropic 不是在做一个"更好用的代码生成工具"。他们在做的是:以 LLM 为内核的操作系统。
这个内核可以调用工具(系统调用),管理进程(子 Agent),有文件系统(Transcript),有内存管理(上下文压缩),有驱动层(MCP),甚至有自己的"睡眠与做梦"(KAIROS)。
最后说一句
51万行代码,1903个文件,18个安全文件只为一个 Bash 工具,9层审查只为让 AI 安全地帮你敲一行命令。
这是 Anthropic 对"如何让 AI 真正有用"这个问题给出的答案。
他们的答案不是"更大的模型",不是"更好的对齐",而是:建一套完整的信任体系。
你不能把 AI 关在笼子里——关起来的 AI 帮不了你什么。你也不能让 AI 裸奔——裸奔的 AI 迟早给你惹麻烦。
唯一的路是:给它建一套精密的约束系统,让它在约束内自由行动。
这套约束系统的代价,是51万行代码。
而这只是 2026 年初的版本。
如果你觉得这篇有意思,可以去翻翻 Claude Code 的 npm 包,虽然现在 source map 应该已经被摘掉了——但好消息是,有人已经存了下来。