资讯详情

资讯详情

Claude Code逆向分析:从npm包源码到内部实现

提到逆向很多人第一反应是Android APK、游戏外挂、Jar包反编译。但如果你把Claude Code从npm上拉下来会发现它本身就是一坨Node.js代码源码就躺在你本地。我抱着“官方文档没写清楚那就自己扒”的心态把这个工具的克里实际实现从头到尾捋了一遍。这篇文章不涉及任何破解、绕过授权之类的东西纯粹是记录一次读源码的过程怎么定位关键逻辑、怎么在压缩代码里最快找到自己关心的东西以及最后那几处让我没想到的设计。这篇内容适合已经装过Claude Code、好奇它内部到底怎么跑的人也适合正在做AI Agent、终端CLI或者想对Claude Code做二次开发的工程师。如果你连它怎么安装都还不熟建议先装一遍、跑通最简单的对话再回来看体验会完全不一样。1. 为什么我要对Claude Code做逆向因为文档讲故事代码讲真相1.1 官方文档留白的地方恰好是我最想知道的Claude Code的官方文档写得很工整怎么安装、怎么配置环境变量、怎么让它读代码库、怎么允许它执行命令一条一条很清楚。但实际用起来有几个问题文档始终没给我答案。我遇到的第一件怪事是明明在终端里导出了ANTHROPIC_API_KEYCLI 启动后还是弹出登录提示要求我用 Claude 账号走 OAuth。当时我以为是环境变量名写错了反复检查了好几遍都在。最后我只能连系统环境变量、当前 shell 的环境、还有配置文件里的字段全查了一遍仍然没搞明白它为什么不用我给的 Key。第二件事是权限确认。Claude Code 在读取文件、写文件、执行 Bash 命令之前会蹦出允许/拒绝的提示。允许过一次之后下次同一类命令它就不再问了。这个“同一类”到底是怎么定义的是按命令前缀还是按整个命令行还是按目录文档说的是“它有自己的安全机制”一句话就带过去了。第三件事是会话历史。我很好奇它把多轮对话存在哪里、用的什么格式、能不能直接导出。文档也没有提。这些问题的共同点是只有读源码才能给出确定答案。所以与其继续翻文档不如把包拉下来自己看。1.2 “逆向”在这里不是破解而是读交付物我没去搞什么内存 dump、动态调试那一套因为Claude Code是通过npm分发的它本质上就是一个JavaScript包。你在node_modules或 npm tarball 里看到的东西虽然经过了转译、压缩变量名变成了t、e、n这种但它的字符串常量、JSON配置、模块结构都还留在那里。这比逆向一个编译型二进制要温柔得多。这也算一种“逆向”从可执行交付物反推设计决策。很多从业者习惯把这个过程叫“源码分析”而不是“逆向”但本质上都一样——你不再接受黑盒的说法选择自己把盒子撬开看里面齿轮是怎么咬合的。我这里采用的方式是直接从npm包入手而不是从GitHub仓库。原因后面会细说。2. 先从npm包下手比从GitHub下手靠谱得多2.1 用 npm pack 把线上包固定拉下来很多人一听说分析源码第一反应就是去GitHub找仓库。Claude Code相关的代码确实部分能在开源仓库里看到但npm上分发的那个包才是用户实际运行的东西。仓库里看到的和实际跑的可能已经不是同一份代码。所以我选择用npm pack拉一枚完整的tarball锁定当时的版本。npm view anthropic-ai/claude-code version npm pack anthropic-ai/claude-code mkdir -p decode-cc tar -xzf anthropic-ai-claude-code-*.tgz -C decode-cc cd decode-cc/package用npm pack而不是直接npm install是因为pack拿到的是一个干净的交付物不会因为本机已经装了别的依赖而混入其他文件。而且tarball自带版本号分析完知道自己在看的是哪个版本后续想重现结论也容易。拿到包之后第一件事不是急着解压缩完就去看代码而是先看这次的版本号。版本差异这个问题后面我会单独讲。2.2 package.json 才是第一块拼图先看 bin 和 dependencies解开tarball后里面是package目录最显眼的是package.json。我习惯先把这个文件格式化后单独输出关键字段node -e const fs require(fs); const pkg JSON.parse(fs.readFileSync(package/package.json, utf8)); console.log(JSON.stringify({ version: pkg.version, bin: pkg.bin, dependencies: pkg.dependencies, engines: pkg.engines }, null, 2)); bin字段告诉我们用户在终端敲claude命令时实际执行的是cli.js。dependencies字段里出现anthropic-ai/sdk这种字样说明API调用部分被拆到了SDK包里CLI层主要负责交互、参数组装和本地文件管理。这几个字段一出来整个代码的粗粒度结构就有了文件/依赖作用猜测cli.js可执行入口命令解析、交互循环入口anthropic-ai/sdk和API服务端通信的客户端SDK其他运行时依赖终端UI渲染、文件监听、配置解析、命令行交互有了这张地图接下来就不会在几百个文件里迷路。2.3 打开 cli.js 时的第一印象压缩、转译但还没到不可读cli.js这个文件需要先看下体量wc -l cli.js ls -la | grep -E sourcemap|\.map我当时这个版本的文件接近上万行。如果你期待看到一个风格良好的源码工程那会失望。它的形态是转译后的CommonJS函数名被缩写成单字母缩进被压平肉眼几乎没法直接阅读。但有个好消息没有做真正意义上的混淆。字符串常量都还在类名、方法名尽管被缩短但API消息里的字段名、配置文件名、环境变量名这些是不能乱改的否则程序就跑不起来了。所以逆向的关键策略就变成了放弃读变量名改为抠字符串、追调用点。另外我检查了包里有没有.map文件。Sourcemap 如果存在可以直接还原出接近原始 TS 的代码。这次我拿到的包里没带 map所以只能走字符串搜索这条路。如果以后你拿到的包带了 map用source-map这个 npm 包就能把产物转化成可读性高得多的代码。3. 顺着字符串搜索我挖到的几个关键实现细节3.1 API密钥与登录态源码里有一套明确的读取优先级我搜的第一个关键词是ANTHROPIC_API_KEY。这个搜索基本一秒就定位到了环境变量读取逻辑。顺着调用点往上翻可以看到一个非常清晰的优先级链优先读取process.env.CLAUDE_CODE_ANTHROPIC_API_KEY。如果没拿到再读process.env.ANTHROPIC_API_KEY。如果还是没有继续读本地的配置文件。最后才转向已经登录保存的 OAuth 凭证。这里就有意思了在用户同时设置了环境变量和已经登录 OAuth 的情况下环境变量里的 Key 优先级要高于 OAuth 登录态。但我前面遇到的“明明设置了 Key还弹登录”这个问题源码给出的答案是配置文件名和字段名可能不对。我去翻配置文件读取逻辑时看到它有倒装读取多个配置来源的过程。如果只写了ANTHROPIC_API_KEY环境变量但拼写成了ANTHROPIC_API_KEY_多了一个下划线或者写进了错误位置的配置文件CLI 会认为没有 Key于是回落到了 OAuth 流程。这个问题从文档上很难快速定位但源码里一眼就能看清function resolveApiKey() { return process.env.ANTHROPIC_API_KEY || getSettings().apiKey || getAccountToken(); }这里想提醒一句如果你也用Claude Code做过二次封装或脚本化调用最好先确认自己的Key来源走的是哪一条通道否则可能在日志里看到一堆诡异的权限报错实际启动变量根本没生效。3.2 Agent Loop 真的是一个循环消息数组不断追加 tool_resultClaude Code 这类工具的核心不是“一问一答”而是一个消息循环。我搜索tool_use和tool_result很快就找到了主循环逻辑。简化后的流程是这样的async function runAgent(client, messages, tools) { let shouldStop false; while (!shouldStop) { const response await client.messages.create({ model: claude-sonnet-4-20250514, messages, tools, system: systemPrompt, }); messages.push(...response.content); const toolUses response.content.filter( (block) block.type tool_use ); if (toolUses.length 0) { shouldStop true; break; } for (const block of toolUses) { const result runTool(block.name, block.input); messages.push({ type: tool_result, tool_use_id: block.id, content: result, }); } } }这个循环的关键在于工具执行结果不是存到什么额外状态里而是作为普通消息追加回对话历史。这样模型下一轮读取时看到的就是一份包含了工具结果的完整上下文。也就是说Claude Code 并没有维护一个特殊的“工作流状态机”它维护的是一份永不停更的消息数组。工具调用只是往数组里塞了两类标准消息tool_use和tool_result。这个模式我在做自己的 Agent 时直接照搬过稳定且好调试。3.3 工具调用前的权限判断不是写死的而是一条可插拔的检查链Claude Code 在执行 Bash 命令前会问用户是否允许。源码里这块设计得比我想象中要清晰。搜索permission、allowlist、denylist之后可以看到工具执行前会经过权限管理器。它的核心逻辑概括起来就三层层级判断依据结果第一层命令是否命中用户明确允许的 pattern直接放行第二层命令是否命中用户明确拒绝的 pattern直接拒绝第三层是否在非交互环境下运行非交互模式走默认策略可能是全拒绝或全允许这个设计让我想起 CI 系统里的安全沙箱交互模式下把决策权交给用户非交互模式下用默认策略兜底。我后来在给自己写的自动化工具加权限模块时也复刻了这个三层结构确实能减少误伤。值得注意的一个细节是允许/拒绝的判断依据并非“命令前缀”而更像“完整命令模板”。用户允许过一次npm test下一次npm test -- --watch可能仍然会触发询问因为模板不完全一致。这一点在文档里没有细讲但源码里能看出来。如果你在用Claude Code做自动化执行可以主动把常用命令加入允许规则减少不必要的打断。3.4 本地会话数据settings.json 和其他周边文件到底存了啥关于会话历史我在代码里搜索~/.claude、settings.json这类路径找到了不少本地落盘逻辑。它把配置和状态拆成几个文件职责划分很清晰文件路径大致内容~/.claude/settings.json用户级配置比如权限规则、模型偏好~/.claude.json按项目维度保存的会话状态和工具调用记录会话历史相关目录多轮对话的消息记录、时间戳、执行结果这个拆分的思路是全局设置和项目级状态分开不会因为 A 项目的配置污染 B 项目。我的直观感受是它把“配置”和“状态”做了比较彻底的隔离这点比很多同类工具做得干净。需要提醒的是这些文件里如果包含 OAuth 凭证属于敏感信息。做逆向分析时可以看结构、看字段名但别把别人的.claude.json到处贴也尽量别在自己机器上关掉权限就为了看明文。安全底线还是要有的。4. 在压缩代码里捞针我踩过的三个坑和方法4.1 别一开始就试图读全部代码先抠字符串拿着一个上万行的压缩文件如果从第一行开始读大概半小时就会想放弃。我的做法是先提取所有字符串常量通过高频字符串反推模块边界。grep -oE [^]{4,} cli.fmt.js | sort | uniq -c | sort -rn | head -50跑完这条命令你会得到一段出现频率极高的词云tools、model、permission、ANTHROPIC_API_KEY、tool_result、settings.json。这些词就是代码的骨架。想找认证就顺着ANTHROPIC_API_KEY搜想找循环就顺tool_use搜想找本地缓存就顺settings.json搜。这一步做完基本就不用再看其他无关代码了。4.2 注意区分 CLI 层和 SDK 层别把脏水泼错地方Claude Code 这个 npm 包虽然入口是cli.js但它真正和API服务端通信的逻辑在anthropic-ai/sdk里。也就是说如果你发现请求头不对、超时逻辑诡异先想一下这个问题是出在CLI拼装参数的过程还是出在SDK发送请求的过程。我一开始就在cli.js里找 HTTP 请求超时配置找了半天一无所获。后来打开node_modules/anthropic-ai/sdk才发现超时和一些重试逻辑在 SDK 的client.ts编译后里。这个教训是逆向一个包之前先把它的依赖树理清楚尤其是包含别人SDK的情况否则很容易把时间耗在错误的地方。4.3 版本漂移问题这次逆向的结论要标上版本号Claude Code 的迭代节奏很快一周可能更新好几个版本。我分析的是当时最新版但等下个月再回头看可能认证逻辑已经变了。所以我的习惯是用npm pack固定副本把 tarball 留在分析目录。在笔记开头记录npm view拿到的版本号。以后若要复现结论就用同一个 tarball而不是最新线上包。这样的分析才有可复现性。如果有人跟你说“Claude Code 源码我看到什么什么”你至少要问他版本号否则很难判断说的是不是同一个东西。5. 这篇逆向分析之后我实际拿来做了什么5.1 它帮我解决了一个困扰我一周的认证问题回到文章开头说的那个问题配置了环境变量CLI 仍然尝试 OAuth 登录。看了源码后我才发现它读取 Key 的优先级是先读CLAUDE_CODE_ANTHROPIC_API_KEY再读ANTHROPIC_API_KEY。我当时在全局配置里定义了一个冗长的变量名结果变量名和源码里要找的不一致导致它始终认为自己没有 Key。定位到原因后改环境变量名重启 shell问题立刻消失。如果没有源码排查我大概还会在配置文件名、权限、防火墙这些方向继续转圈。5.2 我把它的消息循环架构用到了自己的工具里向源码学到一个最实用的点就是前面那个消息数组循环。我后来照着这个模式写了个简化版Node脚本用来在自己项目里跑简单的Agent任务const result await runAgent(client, [ { role: user, content: 看看 src 下的代码给我总结一下 }, ]); async function runAgent(client, initialMessages) { const messages [...initialMessages]; while (true) { const resp await client.messages.create({ model: claude-sonnet-4-20250514, messages, tools: [{ type: tool_use, name: read_file }], }); messages.push(...resp.content); const toolUses resp.content.filter((b) b.type tool_use); if (toolUses.length 0) break; for (const tool of toolUses) { const content tool.name read_file ? readLocalFile(tool.input.path) : unknown tool; messages.push({ type: tool_result, tool_use_id: tool.id, content, }); } } return messages; }这套模式的好处是所有的中间过程都留存在消息列表里出问题时可以完整回放相当于自带日志。目前我用它处理一些需要自动读文件、判断、再操作的场景跑得相当稳。5.3 逆向可以做和不可以做的边界源码分析这种方向边界其实很清楚你可以看代码、理解实现、学习架构也可以基于这些知识做合法的二次开发。但拿着逆向结论去破解订阅、绕过授权、批量请求、盗用凭证这些越界行为我不能支持也不建议任何人做。Claude Code 本身允许用户在本地读取 npm 包内容这是它的分发方式决定的但尊重服务的条款和付费边界是每个从业者该有的底线。我个人觉得这次逆向深扒最大的收获不是某个具体函数怎么写的而是它让我养成了“先看代码再下结论”的习惯。文档和技术博客写得再详细也是别人转述的实现结果源码才是离真相最近的地方。下次你再碰到一个神秘的CLI工具不用急着发帖问“它为什么会这样”拉下来自己搜几个字符串答案往往比你想的要简单。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →