不少开发者习惯把 Claude Code 当作一个终端下的 coding agent,但这次流传出来的源码揭示了更完整的面貌:它实际上是一套分层完整的本地 agent 运行时。
事情的起因并不复杂。Anthropic 在 npm 上发布的 @anthropic-ai/claude-code 包中,附带了一个将近 60MB 的 cli.js.map 文件。社区根据 source map 里的 sourcesContent,还原出了大约 1884 个 TypeScript 源文件。虽然这并非完整的内部仓库,但已经足以看清核心架构。
Claude Code 标题图
综合这批材料,可以先确认两点:
- Claude Code 的核心 agent loop 并不复杂
- 复杂度主要分布在 loop 外围:终端 UI、工具运行时、权限系统、上下文治理、缓存以及远端 transport
npm 包里究竟泄露了什么
先看 npm 包本体。
Claude Code npm 包与 source map 截图
最具信息量的不仅是 cli.js.map,还包括包内其他文件:README、bun.lock、sdk-tools.d.ts、vendor/。这说明发布到 npm 的并非一个轻量包装器,而是一份承载完整产品逻辑的分发包。社区获得的也不只是压缩后的 CLI 二进制,而是足以拆解产品骨架的 npm 包。
再看还原后的 src/ 顶层目录结构。
Claude Code src 目录截图
仅看目录名称,就能大致划分出架构:
entrypoints/:不同的启动入口main.tsx:主运行时入口点screens/、components/、context/:终端 UI 层services/:工具服务、MCP、分析及其他运行时服务skills/、plugins/、commands/:扩展模块bridge/、remote/:远端控制与远端会话assistant/、coordinator/:自主 agent 与多 agent 逻辑buddy/、voice/、vim/:功能分支与实验特性
仅从这一层就能看出,Claude Code 绝非一个简单的命令行包装器。它更像一个以终端为首要界面的本地应用——前端是一套 TUI,后端则是完整的 agent 运行时。
终端 UI 为何采用 React + Ink
初次浏览这些文件时,你可能会产生疑问:为什么入口文件叫 main.tsx?为什么目录里有 context/?为什么一个命令行工具到处充斥着 React 组件?
答案在于它的终端 UI 使用的是 Ink——即 React 的终端渲染器。
一旦理解了这一点,许多代码便豁然开朗:
- 权限确认框是一个组件
- 工具调用展示是一个组件
- Markdown 渲染是一个组件
- REPL 本身也是一个前端界面,只不过运行在终端里
所以 main.tsx 并非命名习惯问题,它就是该产品的前端入口之一。代码中还能看到与滚动性能相关的处理:用户滚动时,后台工作会短暂暂停,定时器会执行 unref(),以避免阻止进程退出。这类处理更像桌面前端,而非普通 CLI。
换句话说,在 Anthropic 的实现中,终端并非“文本输出设备”,而是另一个前端容器。
主执行链路的结构
将主执行路径精简后,大致如下:
entrypoints/cli.tsx先处理快速路径main.tsx完成完整初始化replLauncher.tsx挂起App与REPLREPL.tsx组装消息状态、工具池、权限状态、MCP 状态- 输入被传入
query(...) query.ts执行 turn loop,处理流式输出、工具调用、上下文整理toolOrchestration.ts负责编排调度toolExecution.ts负责具体执行
这里容易产生误解:仓库中虽然已有 QueryEngine.ts,但 REPL 当前主路径仍然走 query()。QueryEngine 更像是为 SDK / headless 场景及后续抽象预留的层次。
也就是说,Claude Code 并非“一开始就将所有统一为同一套引擎”,而是先稳定 REPL 路径,再将共享能力逐步抽象出来。
Tool 在这里并非普通函数
在这批源码中,Tool 抽象占据了很大比重。
许多人在自行编写 agent 时,工具通常只是:一个 schema、一个 handler、一个返回值。但 Claude Code 中的工具尺度完全不同。Tool.ts 里一个工具对象除了 name、inputSchema、call() 之外,还必须包含许多额外内容:
prompt():如何向模型描述自身checkPermissions():权限判断逻辑isReadOnly():是否只读isConcurrencySafe():能否并发执行isDestructive():是否具有破坏性renderToolUseMessage():调用时的展示方式renderToolResultMessage():结果展示方式searchHint:如何被检索到interruptBeha vior:中断时的处理行为
在 Claude Code 中,一个工具并非单纯函数,而是一个同时覆盖提示词层、执行层、权限层和 UI 层的运行时对象。
buildTool 的默认值也很直接。如果新工具未显式声明安全性,系统不会乐观假设:isConcurrencySafe 默认为 false,isReadOnly 默认为 false。这意味着默认将其视为“不可并发、可能写文件”的工具。权限层面同样保持保守方向,不会因为作者忘记声明就自动变为安全工具。
工具调度分为两层
从执行链路来看,Claude Code 的工具部分至少拆分为两层。
toolOrchestration.ts
这一层先进行工具编排,将同一轮模型输出中的工具调用分为两类:可以并发执行的,以及必须串行执行的。它不直接做权限判断,也不直接执行命令,主要负责“谁和谁一起跑,谁需要排队”。
toolExecution.ts
真正将一次调用落地的地方。它需要处理很多事项:输入校验、工具层面校验、权限检查、hook 决策、telemetry、MCP 特殊处理、执行结果回写。
这批泄露材料中还有一个关键细节:工具并非等到整段模型回复结束才开始执行,而是在流式输出期间就启动。
这样做有两个直接好处:第一个工具可以更早启动,可并发的工具也能提前完成。
但这带来了恢复问题:如果模型在流式过程中 fallback 到另一个模型,已经启动的工具怎么办?Claude Code 的处理方式是:在中间 fallback 时,将已经启动的工具停止,然后整轮重新开始。它牺牲了一部分实现简洁性,换取了整轮状态的一致性。
Bash 安全:纵深防御体系
这套源码中最像“线上事故沉淀”的部分,是 Bash 工具相关代码。
因为“让模型执行终端命令”本身就是整个系统中风险最高的能力之一,所以这里的设计并非简单黑名单,而是一条纵深校验链。
bashSecurity.ts 这类逻辑中包含许多独立检查器,分别针对不同攻击面:空命令、heredoc 与命令替换、shell 元字符、危险变量、重定向、Unicode 空白字符、回车注入、$IFS 注入、/proc/*/environ 访问、大括号展开、注释和引号不同步。
其中有两类问题尤为典型。
1. Unicode 空白字符
某些字符看起来像空格,但 shell 并不将其视为普通分隔符。人眼看到的是正常命令,而解析器理解的是另一回事。
2. 回车注入
\r 能让终端后续内容覆盖前面的显示。屏幕上显示的是安全命令,实际执行的可能是另一条。
再往下看,还有一层针对 Zsh 的特殊治理。因为 macOS 默认 shell 为 Zsh,而 Zsh 拥有不少 Bash 所不具备的内建能力,可能绕过普通二进制检查。Claude Code 对这些路径单独做了限制,防范的不是一般误操作,而是有意绕过的行为。
权限系统横跨四层
Claude Code 的权限并非一个简单的弹窗函数,而是一套公共层次。
从代码分布来看,至少有 4 层:
1. Tool.ts
这里定义权限上下文,不仅包含 allow/deny/ask,还包括:mode、additional working directories、bypass/auto mode、prompt beha vior flags。
2. permissionSetup.ts
这一层决定初始权限包络。CLI 传入的 allow/deny 规则、基础工具规则、当前模式,都会在这里先整理好。
3. useCanUseTool.tsx
这是运行时与 UI 的接口层。它要决定的不仅是“能不能用”,还包括:直接放行、直接拒绝、弹出权限框、转给协调器、转给 worker、在某些路径下走自动分类或自动拒绝。
4. toolExecution.ts
真正执行前,还会再次读取当前 app state、hook 决策和权限状态。
这套分布看似较重,但逻辑上是连贯的:权限并非某个工具独有的事务,而是所有工具、本地模式、远端模式、后台 worker 以及 UI 都需要共享的语义。
泄露材料中有一句话清晰概括了后台 agent 的约束:无法向用户弹框确认的后台 agent,遇到需要审批的能力就直接拒绝。拿不到确认,就不执行。这与此套权限系统的整体取向完全一致。
上下文窗口治理:不止一次摘要
Claude Code 在处理上下文窗口时的做法,与许多人写 agent 时的第一反应不同。
很多自制 agent 的做法是:快超限时,做一次压缩或 summarize。
Claude Code 更像一条固定的降级链:
- 先截断工具结果
- 再裁掉旧消息
- 再按消息组做摘要
- 如果还不够,再单独发起一次模型调用,对整段会话做更重的压缩
这 4 步并非处理同一个问题:工具输出过长、普通对话历史过长、历史结构尚在但体积过大、整体上下文已必须压缩。
这也是为什么许多简易 agent 在短任务中表现良好,但长任务时开始偏离——它们做了“摘要”,却没有分层降级。
Prompt cache 与成本控制直接融入架构
这次泄露中还有一块非常实在的内容,即 prompt cache 相关逻辑。
Claude Code 的系统提示很长。如果每轮都原样重发,成本会很高,因此代码中专门设置了一条动态边界,将系统提示分为两部分:前半部分是稳定内容,后半部分是工作目录、模型名、MCP 配置等动态内容。这样,stable prefix 更容易命中缓存。
更精细的一点是,与请求头相关的某些状态会被做成“锁存器”。一旦某个模式打开,对应 header 就不再来回切换,因为这种切换会破坏 prompt cache。
会话在 API 调用前就持久化为 transcript,也属于同一思路。表面上看是为了 --resume 恢复,实际上也减少了进程意外中断后整段对话重发的成本。
Skills、Plugins、MCP 最终被收入统一命令面
如果仅看 README,很多人会将 skills、plugins、MCP 视为三种并排的功能。
但从 commands.ts 和 loadSkillsDir.ts 可以看出,它们最终都被收入同一张命令注册表:
- filesystem skills
- plugin skills
- bundled skills
- built-in plugin skills
- plugin commands
- workflow commands
也就是说,Claude Code 并非在主程序边上挂几个扩展点,而是将这些来源统一成一个命令面。
loadSkillsDir.ts 还表明 skills 的来源非常多样:managed policy 目录、用户设置目录、项目目录、--add-dir、legacy commands-as-skills。而且技能可以根据文件路径动态激活。MCP 这边也不是 sidecar 式接入,客户端会直接将 MCP server 暴露的工具和资源收入运行时。
这一层设计决定了 Claude Code 后续的可扩展能力为何能持续增长,因为底座并非一次性写死的。
bridge/ 和 remote/ 表明它已不仅限于本地 REPL
源码中至少能看出两套不同的远端栈:
bridge/
这是将本地机器变为远端控制 worker 的层次。包含:feature gating、entitlement/OAuth/policy 检查、基于环境变量或无需环境变量的路径选择、v1/v2 transport 适配、worker 生命周期管理。
remote/
这是让本地客户端连接并显示远端 session 的层次。可以看到:websocket 订阅、HTTP message send、permission request/response handling、远端权限请求到本地 UI 的桥接。
因此,这两套栈并非一回事:bridge/ 是“本地机器被远端控制”,remote/ 是“本地客户端连接远端 session”。而它们最终仍需回到同一套工具语义和权限语义上。这也说明了为什么 Claude Code 的产品边界早已不限于单一本地 REPL。
多 agent 与未发布能力,指向同一条产品延长线
本次泄露中最具传播性的,仍是这些名字:KAIROS、Auto-Dream、ULTRAPLAN、SPECULATION、BUDDY。
其中确实有轻松的元素,例如 BUDDY。源码中呈现了一整套 ASCII 宠物系统,物种、帽子、眼睛、稀有度、名字、性格均已完成,随机方式甚至按用户 ID 做确定性生成。
但这些名字更重要的价值,并非“Claude Code 里藏着什么彩蛋”,而是它们几乎都建立在现有底座之上:KAIROS 指向跨 session 常驻 agent,Auto-Dream 指向空闲时自动整理记忆,SPECULATION 指向预测下一步输入并提前执行,ULTRAPLAN 指向更重的远端规划和多 agent 探索。
而多 agent 层次,在泄露材料中说得非常直白:sub-agent 本质上就是同一个 turn engine,只是换一组参数再运行一遍:换模型、换工具集合、换权限上下文,必要时换 git worktree。
这说明“多 agent”未必意味着重造一套全新系统。很多时候,只要先把 turn engine、权限语义、工具语义和工作区隔离做稳,后面的 worker/coordinator 只是对同一系统的参数化复用。
这批源码中可以直接观察到的工程取向
将这次流出的源码材料从头到尾梳理一遍,最终留下的并非某个神秘 loop,而是这些工程取向:
- 终端 UI 被视为前端来编写
- 工具被设计为完整的运行时对象
- 工具调度支持流式期间启动和整轮重启
- Bash 风险使用纵深防御处理
- 权限系统横跨 setup、runtime、UI、remote 4 层
- 上下文窗口治理是一条分层降级链
- Prompt cache 与成本控制直接写入架构
- Skills、plugins、MCP 被收入统一的扩展平面
- Bridge 与 remote 证明它早已不是单一表面产品
Claude Code 这次最值得关注的,并非“它能不能调用工具”,而是它已经将终端 UI、工具、安全、缓存、权限、扩展和远端 transport 整合成了一套完整产品。
