资讯详情

Claude Code源码拆解:主循环、工具注册表与权限机制实践指南

📅 2026/10/17 17:04:13 | 华诺云谱 👁 阅读
Claude Code源码拆解:主循环、工具注册表与权限机制实践指南
简介这是一份 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: Recordstring, any; execute(input: unknown, ctx: ToolContext): PromiseToolResult; } 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 源码我都先画主循环再看工具注册表踩过的坑基本都收在上面五个里。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑