简介:这是一份 Claude Code v2.1.88 完整 TypeScript 源码包,源自 npm 包 @anthropic-ai/claude-code 解包,适合 AI 应用开发者、终端工具研究者深入剖析大模型代码助手内部实现。资源共 1968 个文件,以 1340 个 ts 与 552 个 tsx 源码为主,辅以 js、md、json 等配置与文档,压缩包约 19MB,目录结构完整。包内 src 目录含 37 个子目录、约 51 万行代码,覆盖查询引擎、工具系统、命令处理、React UI 等核心模块;docs 目录附带遥测与隐私分析、隐藏功能与模型代号、卧底模式、远程控制与 KillSwitch、未来路线图等深度分析文档。另有 40 余个内置工具实现、MCP 协议支持与工作流脚本系统,可帮助读者理解 Agent 循环、工具调用和终端交互设计。已有 444 人学习下载,是一份难得的 AI Agent 工程实践参考资料。
1. 拿到 Claude Code 源码包,先回答两个问题再动手
我第一次解压这类“源码+分析文档”的学习包时,没有急着跑 npm install,而是先问自己两个问题:这份源码的核心循环在哪里?工具注册表长什么样?因为 Claude Code 本质上不是一个“调一次模型就返回答案”的普通封装,它是一个带领 Agent 循环、工具分发、权限判定、会话管理的命令行应用。如果把这几个关键词理解了,后面的安装调试和二次开发都有了框架。你之所以会搜“Claude Code 源码”“src”“分析文档”,多半不是想装一个现成工具,而是想知道它内部怎么工作、能不能改、能不能把默认模型换成自己的服务。这篇文章就按“是什么 → 怎么跑 → 怎么读 → 坑在哪 → 怎么改”这条路,把整套源码拆给你看。
2. 拆 src 前先立住两个心智模型:主循环与工具注册表
很多人第一次翻这套源码会翻车:翻遍目录找不到“模型思考的核心算法”,然后开始怀疑自己拿到的包是不是缺文件。其实不是缺文件,是心智模型错了。Claude Code 这类编码代理,核心不是某个惊艳的算法函数,而是一个不复杂但极其关键的主循环,加上一张统一形状的工具表。先把这两个模型立住,src 里的绝大多数文件都能对号入座。
2.1 它不是一个“模型调用程序”,而是一个带权限的 Agent 循环
常见的理解误区是:把它当作“用户输入一句话 → 调一次模型 → 输出答案”的三行脚本。真正跑起来的是一个循环:
- 把完整的对话历史(transcript)发给模型;
- 模型返回两种情况:要么是一段最终文本,要么是一组工具调用请求;
- 如果有工具调用,遍历每个调用,先做权限判定,通过后分发到对应工具执行;
- 把工具执行结果作为 tool_result 追加到对话历史里;
- 带着新历史回到第 1 步,直到模型不再请求工具,或者达到最大轮数。
这个循环决定了源码里几乎所有文件的职责。你会在 src 里看到会话(session/transcript)相关的文件,因为它们要负责序列化、压缩和恢复对话历史;会看到工具(tools)相关文件,因为每个能力都是一张可枚举的表;会看到权限(permission)相关文件,因为在执行任何工具前都要先裁决。分析文档通常在开头就画这条链路,读源码时先把它钉在脑子里,后面不会迷路。
2.2 src 的三条主线:会话、工具、权限
顺着上面的循环,src 内部可以拆成三条主线来读。
第一条是会话线。Claude Code 每轮都要把完整历史发给模型,历史里既有普通的用户消息、助手消息,也有工具调用消息和执行结果。源码里会花不少篇幅处理“历史太长怎么办”和“断点恢复怎么办”,这部分代码在文档里通常标注为会话或摘要(summary/checkpoint)模块。读的时候不用抠细节,知道它是把结构化消息存下来、在超长时做压缩就够了。
第二条是工具线。Bash、文件读写、代码搜索、子代理,这些能力在源码里都是同一种东西:一条工具记录。每条记录包含四个字段:名字、描述、输入 JSON Schema、执行函数。模型只能通过名字、描述和 Schema 来理解这个工具,执行函数则被分发器按名字调用。你把这条线搞明白,后面新增一个自定义工具就是十分钟的事。
第三条是权限线。每次工具调用不是直接执行的,要先进权限裁决。常见模式有四种,源码里通常会用枚举或常量表示,对应关系大致如下:
| 权限模式 | 典型行为 | 适用场景 |
|---|---|---|
| default | 敏感命令和文件写入弹确认,只读操作自动放行 | 日常交互 |
| acceptEdits | 文件编辑直接放行,命令执行仍确认 | 只写代码的会话 |
| plan | 只读工具放开,写操作和命令执行不放行 | 先出方案再动手 |
| bypassPermissions | 全部放行 | CI 自动化、批量任务 |
分析文档的价值在这里体现:它一般按“入口 → 主循环 → 工具 → 权限”组织章节,等于给了你一张路线图。遇到和 src 对不上的模块名,别慌,先按住路线图找行为特征,比如“哪个函数在调用工具前读设置项”,比按文件名硬找可靠得多。
3. 在本机把 src 跑起来:安装、构建到打断点的完整路径
源码拿到手里,先跑起来比先读更划算。跑通了,你才有地方打断点、加日志、验证猜测。下面这套是我本机上手时用的最小路径,从环境检查到断点调试一次走完。
3.1 版本与环境检查,决定你后面能不能少折腾
Claude Code 是 Node 技术栈,源码用 TypeScript 编写,所以环境的核心只有两样:Node 运行时和 npm 包管理器。很多人在解压后直接 npm install 爆一堆错,根因往往是 Node 版本不对。先执行下面三条命令:
node --version npm --version npm config get prefix第一行确认 Node 版本,建议使用 LTS 版本,源码和分析文档一般会标注最低版本要求,通常不低于 18。版本太老,语法解析直接失败;版本太新,个别原生依赖的行为也可能对不上。第二行看 npm 版本,npm 太老会影响锁文件解析。第三行看全局安装路径,这个路径在后面处理自动更新权限问题时要用到,先记下来,别等报错了再查。如果 Node 版本不合适,建议用版本管理工具在用户目录装一个 LTS 版本,避免污染系统环境。
3.2 最小化安装与启动:npm ci、build、node dist
环境确认没问题后,进入源码目录执行:
cd claude-code-src npm ci npm run buildnpm ci和npm install的区别值得说一下:npm ci严格按 lockfile 安装,依赖版本完全可复现,适合第一次拉源码跑通;“install” 则会重新解析依赖树,版本可能有偏差,我一般只在排查依赖问题时才用。npm run build把 TypeScript 编译到产物目录,具体输出到 dist 还是 out,以包里的 package.json 为准。构建完成后,直接用 node 启动,不急着做全局安装:
node dist/cli.js --version node dist/cli.js --help先看版本是否正常输出,再看帮助信息里列出了哪些命令。如果这两条能跑通,说明构建链路正常。第一次跑真实任务前,需要把模型 API 密钥放到对应的环境变量里,具体变量名以你拿到的分析文档标注为准;不同发行版可能用不同名字,别凭记忆猜。设置好后,跑一条最简单的对话验证通路:
node dist/cli.js "用一句话告诉我当前目录里有哪些文件"这条命令会走到真实模型调用,返回结果说明整条链路已经打通。此时再去读源码,每个模块的输入输出你都见过真实样子了,理解成本会低很多。
3.3 加断点看主循环:inspect-brk 与两种断点位置
跑通之后,我在本机验证源码行为时会用 Node 的调试协议,而不是到处堆日志:
node --inspect-brk dist/cli.js "帮我列出现有文件"--inspect-brk的作用是让 Node 启动后停在第一行代码上,等待调试器连接,默认监听 9229 端口。此时打开代码编辑器,选择“附加到进程”或“Attach to Node”,连上后就能逐行执行了。
读 Claude Code 这类 Agent 源码,建议把断点设在这两个位置:第一,模型调用函数入口,在这里看发给模型的 messages 长什么样、有没有带上工具定义;第二,工具分发函数,在这里看系统是如何把 tool_call 里的名字映射到具体执行函数的。这两个断点能让你亲眼看到“主循环 + 工具注册表”这两个心智模型落到真实调用栈里。读文档时如果它提到了核心函数名,直接按函数名搜索定位比翻目录快得多。
4. 对照分析文档读三个核心模块:任务循环、工具表、权限判定
跑通之后进入正题:读代码。分析文档的好处是它会替你把骨架抽出来,但文档是静态的,代码是动态的,我的做法是把文档里的关键函数在 src 里定位出来,然后对照下面三个模块逐个确认。这三个模块读透了,整套源码的掌握度就有七八成了。
4.1 任务循环:从用户一句话到第一次工具调用之间发生了什么
分析文档里通常会把主循环简化成下面这样一段逻辑:
async function agentLoop(llm, transcript, registry, permission) { for (let turn = 0; turn < maxTurns; turn++) { const resp = await llm.messages.create({ model: cfg.model, messages: transcript.history(), tools: registry.definitions(), temperature: cfg.temperature ?? 0, }); transcript.push(resp); if (!resp.tool_calls || resp.tool_calls.length === 0) { return resp.text; } for (const call of resp.tool_calls) { const decision = await permission.check(call, ctx); if (!decision.allowed) { transcript.pushToolResult(call.id, { error: "permission denied", }); continue; } const output = await registry.dispatch(call.name, call.input, ctx); transcript.pushToolResult(call.id, output); } } }这段伪代码里最值得注意的参数有三个。第一个是maxTurns,它限制循环最多跑多少轮,防止模型在工具调用里死循环;实际项目里这个值会放在配置里,调小能省钱,调大能处理更复杂的多步任务。第二个是tools,它把注册表里的工具定义整体发给模型,模型看到的是名字、描述、输入 Schema,而不是执行函数本身,这是安全和可控的关键。第三个是temperature,代码生成场景一般设成 0 或很低,因为写代码要确定性,太高会让工具参数生成变得不可预测。
真实的 src 里,这个循环不会像一个函数这么干净,它会被拆成会话管理、模型调用、消息解析、停止原因处理等多个文件,但逻辑骨架不会变。你在 src 里找“谁在循环里调用模型”“谁把 tool_call 结果写回历史”这两个问题,就能反推出循环主体在哪。
4.2 工具注册表:为什么所有能力都长同一个形状
Claude Code 的每个能力,从读写文件到执行命令,在源码里都遵循同一个接口形状:
interface Tool { name: string; description: string; inputSchema: Record<string, any>; execute(input: unknown, ctx: ToolContext): Promise<ToolResult>; } interface ToolResult { content: string; status: "success" | "error" | "cancelled"; isError?: boolean; }这个统一形状是整个架构能扩展的根本原因。名字和描述是给模型看的,输入 Schema 是模型生成参数时的约束,执行函数只关心“拿到输入、干活、返回字符串”。读源码时你会看到每个工具的 description 都写得很讲究,字段说明、格式示例都在,因为这个文本模型真的会逐字读。
这里有个容易忽略的参数细节:inputSchema的字段定义直接影响模型成功率。Schema 写得太死,模型经常生成不出合法参数;写得太松,模型又可能塞进无关字段。源码里那些描述文本特别长的工具,往往是经过反复调优的,你改的时候别轻易精简。另外,subagent 这种看起来高级的能力,在注册表里也不过是一个叫 Task 的工具,它的输入里描述子任务怎么做,执行时再去起一个子 Agent 循环。理解了这层,你就知道挂新能力有多简单了。
4.3 权限判定:安全边界在哪一行代码上生效
权限模块是这套源码里最值得读的安全代码。我用下面这段代码描述它的核心逻辑:
async function resolvePermission(toolCall, ctx, settings) { if (settings.bypassPermissions) return { allowed: true, reason: "bypass" }; if (settings.deniedTools.includes(toolCall.name)) { return { allowed: false, reason: "denied_list" }; } if (settings.allowedTools.includes(toolCall.name)) { return { allowed: true, reason: "allow_list" }; } if (toolCall.name === "Edit" && settings.mode === "acceptEdits") { return { allowed: true, reason: "accept_edits_mode" }; } if (ctx.planMode) { return { allowed: isReadOnlyTool(toolCall), reason: "plan_mode" }; } return await interactiveConfirm(toolCall); }这段逻辑里有三个边界要看清。第一,判定顺序是固定的:先看 bypass,再看 deny 名单,再看 allow 名单,最后才看模式和交互确认。顺序颠倒会导致安全绕过,读源码时确认一下这个顺序没被改过。第二,权限是挂在工具名上的,不是挂在命令字符串上的,这就意味着一个危险的 Bash 命令和一个温和的 Bash 命令,走的是同一条裁决路径;真正的命令内容过滤通常在更底层。第三,路径边界在文件工具里单独处理,源码一般会把传入的路径做归一化后与工作区根目录比对,越界直接拒绝。这个比对发生在权限判定之前还是之后,不同发行版可能不一样,你自己改造时要注意,路径检查必须优先于内容读取。
5. 读源码与二次开发的避坑清单:五个高发问题
这套源码我在本机和调试环境下翻来覆去跑过很多轮,也帮别人排查过不少问题。下面五个坑是我见过出现频率最高的,按“现象 → 原因 → 对策”写清楚,你遇到的时候可以少走两小时弯路。
5.1 现象:npm 报 auto-update failed: no write permission to npm prefix
启动后提示自动更新失败,错误信息大概长这样:auto-update failed: no write permission to npm prefix。
原因很简单:CLI 当初被装到了系统级目录,而当前用户对那个目录没有写权限,更新器尝试用 npm 安装新版本时必然失败。排查和解决的命令如下:
npm config get prefix npm config set prefix "$HOME/.npm" export PATH="$HOME/.npm/bin:$PATH" npm linknpm config get prefix先确认全局安装路径;npm config set prefix把全局目录改到用户目录下,避免操作系统目录;export PATH让终端能找到新位置下的可执行文件;最后npm link把当前项目构建出的 CLI 链接为全局命令。做完这三步,更新权限问题基本消失。如果是跑自动化任务,也可以直接把自动更新关掉,版本锁定在本地验证过的构建上,避免任务中途 CLI 自更新导致行为变化。
5.2 现象:npm ci 之后构建出来的 dist 一启动就报模块找不到或语法错误
源码明明完整,依赖也装完了,但node dist/cli.js一启动就抛 SyntaxError 或 module not found。
原因是 Node 版本不匹配。源码依赖了较新的 JavaScript/TypeScript 语法特性,Node 版本太老解析不了;反过来,少数原生依赖在新版 Node 上行为变化也会报错。解决步骤:
node --version rm -rf node_modules package-lock.json npm install npm run build node dist/cli.js --version先确认 Node 版本并切到 LTS,再删掉 node_modules 和 lockfile 重新解析依赖,因为旧的 lockfile 可能锁了与当前 Node 不兼容的依赖版本。如果重装后依然报错,用node --inspect-brk启动,看异常抛在哪个模块、哪一行,通常比盲改配置快得多。
5.3 现象:改了 src 里的逻辑,重新运行后行为还是旧的
改完 TypeScript 源码,重新跑 CLI,发现改动完全没生效,行为跟官方版一模一样。
根因是 package.json 里 bin 字段指向的是编译产物目录,你改了 src 下的 .ts 文件,但没重新构建,运行的是旧的 dist 文件。另一个常见原因是增量构建没识别到文件变化。对策是强制干净重建:
rm -rf dist npm run build npm link node dist/cli.js --version先删掉 dist 产物目录,再做全量构建,最后无论你是直接用node dist/cli.js还是通过全局命令,都要确认走的是同一份产物。我一般会先看 package.json 的 bin 指向哪个文件,避免自己一直在跑另一个路径下的旧版本而不自知。
5.4 现象:分析文档里写了一个模块名,src 里却找不到同名文件
严格按照分析文档的目录结构去找源码文件,结果发现 src 里根本没有这个文件名,一度怀疑文档和源码是不是同一套东西。
原因是发行版可能把多个源文件打包进了一个入口文件,或者分析文档用了官方内部命名,而手上的 src 经过了重命名、合并。文档是路线图,不是目录树。解决方法是按行为特征搜索,而不是按文件名找:
grep -n "Task" dist/cli.js | head -20把文档里提到的核心函数名、独特的报错字符串、工具名作为关键词去 grep,源码文件虽然变了,但这些字符串特征基本会保留。如果发行包里带了 sourcemap 文件,还能通过 map 文件还原原始目录结构,定位会更准。
5.5 现象:自己加了 console.log 之后,输出变成一团乱码,管道解析失败
调试时往源码里塞了 console.log,然后发现终端输出和日志混在一起,用脚本接 stdout 做结构化解析时直接失败。
原因是 Claude Code 这类 CLI 的输出不是单通道,它至少区分结果流、通知流和错误流。你可能会看到类似下面的通道划分:
| 输出通道 | 典型用途 | 常见误用 |
|---|---|---|
| stdout 结果流 | 最终答案、结构化结果 | 打印日志进去,管道解析直接坏 |
| stdout 通知流 | 进度、状态通知 | 和结果流混在一起,界面错乱 |
| stderr 错误流 | 错误信息和调试输出 | 被忽略,丢排查线索 |
对策也简单:调试日志一律走 stderr 或专门的日志文件,不要污染 stdout 的结果通道。跑任务时把 stderr 重定向到文件里看:
node dist/cli.js "一个真实任务" 2> debug.log想看进度就看终端,想查细节就翻 debug.log,互不干扰。
6. 把改造版变成自己的 CLI:从新增一个工具开始
读源码最划算的一步,是亲手挂一个自己的工具。工具注册表理解了,这个操作二十分钟内就能完成。
先找到注册表的注册区,分析文档一般会标注registerTools之类的位置。然后实现一个很克制的示例工具,读项目内文件并自动加行号:
tools.register({ name: "safe_read", description: "读取项目内文件并加上行号,超长时自动截断,适合快速浏览代码", inputSchema: { type: "object", properties: { path: { type: "string", description: "相对项目根目录的文件路径" }, maxLines: { type: "number", description: "最多返回多少行,默认 200" }, }, required: ["path"], }, async execute(input, ctx) { const full = ctx.resolvePath(input.path); if (!full.startsWith(ctx.workspace)) { return { isError: true, content: "路径越界" }; } const text = await ctx.fs.read(full); const lines = text.split("\n").slice(0, input.maxLines ?? 200); return { content: lines.map((l, i) => `${i + 1}: ${l}`).join("\n") }; }, });这个示例里有三个坑提前帮你避掉。第一,description 写清楚“适合快速浏览代码”,因为模型是靠这段描述决定什么时候用它的。第二,resolvePath之后立刻用startsWith(ctx.workspace)做路径越界拦截,这是从源码里学来的安全习惯,别省。第三,maxLines给了默认值,防止模型不传参时工具把整个大文件读爆。改完执行重建和验证:
npm run build node dist/cli.js "用 safe_read 看一下 README.md 的前 300 行"第一次调用时权限层会把这个新工具当未知工具处理,大概率会弹确认;测试阶段可以把权限模式临时设成 bypassPermissions,验证完立刻关掉,别留着跑真实任务。
我第一次完整读这套 src 的时候,做的第一件事是翻遍目录找“模型的思考算法”,找了一个下午没找到。后来某位做 Agent 基础设施的开发者提醒我:这类编码代理的核心不是算法,是主循环和工具表。我把注意力切到这两个地方,半天就把之前没想通的问题定位了。后来每次拿到一套新的 Agent 源码,我都先画主循环,再看工具注册表,踩过的坑基本都收在上面五个里。希望帮到你。
本文还有配套的精品资源,点击获取