OpenClaw 装过 25 个依赖补丁,5 月 10 日一夜之间全部撤走:13 小时锁定真空与今天的零覆盖状态
两年前主流前端项目还在为 ^ 范围头疼的时候,OpenClaw 在 2026 年 1 月 30 日就干了件更彻底的事:在 package.json 里塞进一块 25 条目的 pnpm.overrides,把包括 qs、tar、axios、uuid 在内的一整批传递依赖钉死在精确版本上。
这块补丁在 npm registry 里连续存在了 152 个版本、约 4 个月。然后在 2026 年 5 月 10 日到 11 日之间,它被拆掉了。此后 125 个连续版本,OpenClaw 的包元数据里再也没有出现过 pnpm 这个字段。
本文拆的是这段安装期依赖钉死层的完整生命周期。所有数字来自 2026 年 10 月 18 日 06:0x 对 registry.npmjs.org/openclaw 全量元数据的解析(278 个版本,非抽样),以及 unpkg 上历史版本 package.json 的原文比对。
一、先说结论:一个字段的四段生命周期
先给整体轮廓,后面每一段都会给出可复算的口径。
| 阶段 | 版本区间 | pnpm.overrides |
顶层 overrides |
packageManager |
|---|---|---|---|---|
| 引入期 | 2026.1.29-beta.1 → 2026.5.10-beta.2(152 个) |
3 → 25 条 | 1 → 7 条 | pnpm 10.23.0 → 10.33.2 |
| 真空期 | 2026.5.10-beta.3 → 2026.5.10-beta.5(3 个) |
已删除 | 保留 7 条 | pnpm 11.0.8 |
| 收缩期 | 2026.5.10-beta.5 → 2026.5.12-beta.1(约 26 小时) |
已删除 | 保留 7 条 | 依赖里的 ^ 同时归零 |
| 现状 | 2026.6.2-beta.1 → 2026.10.4(71 个) |
无 | 无 | 无 |
四段之间最值得停一下的是第二段。pnpm.overrides 在 2026.5.10-beta.2 还钉着 22 个包,紧接着的 2026.5.10-beta.3 就没了——而同一时刻 dependencies 里还有 37 个 ^ 浮动范围没被改成精确版本。这是整个时间线里唯一一段「钉死层已撤、浮动层未补」的窗口。
二、这段字段到底钉了什么
先看 2026.5.10-beta.2 的 pnpm.overrides 全文(22 个非别名条目,另有 3 个别名条目已在下文单列)。这是 unpkg 上该版本 package.json 的原文:

把这份清单和同版本的 dependencies 对照,一张更有信息量的表就出来了:
| 被钉的包 | 出现在 dependencies? | 钉死版本 |
|---|---|---|
@anthropic-ai/sdk |
是(直接依赖) | 0.95.1 |
tar |
是 | 7.5.15 |
minimatch |
是 | 10.2.5 |
file-type |
是 | 22.0.1 |
typebox |
是 | 1.1.38 |
@aws-sdk/client-bedrock-runtime |
是 | 3.1045.0 |
qs |
否 | 6.14.2 |
axios |
否 | 1.16.0 |
form-data |
否 | 2.5.4 |
uuid |
否 | 14.0.0 |
yauzl |
否 | 3.2.1 |
protobufjs |
否 | 7.5.5 |
tough-cookie |
否 | 4.1.3 |
path-to-regexp |
否 | 8.4.0 |
fast-xml-parser |
否 | 5.7.0 |
hono / @hono/node-server |
否 | 4.12.18 / 1.19.14 |
defu / basic-ftp / ip-address / follow-redirects / fast-uri |
否 | 6.1.5 / 6.0.1 / 10.2.0 / 1.16.0 / 3.1.2 |
22 个钉死目标里,只有 6 个是直接依赖,另外 16 个是纯传递依赖。
这一点是理解整件事的关键。dependencies 里写着 ^ 的包,你的包管理器至少知道该去哪个 semver 区间找;但 qs、axios、form-data 这些根本不写在 dependencies 里——它们是被别的包拖进来的第二级、第三级依赖,版本完全由上游包的声明决定。OpenClaw 用 overrides 把这 16 个钉死,等于在正常解析链之外强行开了一条「我说了算」的通道。
顺带一提,钉死的对象本身是有历史包袱的:qs(原型污染)、tar(解压路径穿越)、minimatch(ReDoS)、axios/form-data(SSRF 与泄露)都是反复出现在历史漏洞通告里的包。给这类包钉版本,是当时最常规也最有效的纵深防御动作。
2.1 别名钉法:npm: 与 $ 两种特殊语法
清单里有三种写法不是单纯的「版本号」,容易被当成噪声跳过:
"request": "npm:@cypress/request@3.0.10",
"request-promise": "npm:@cypress/request-promise@5.0.0",
"node-domexception": "npm:@nolyfill/domexception@1.0.28"
这是 npm 的包别名语法:装 @cypress/request,但在依赖树里以 request 的名义存在。request 是 2020 年就停止维护的老包,很多老包还在依赖它;OpenClaw 选择把它的实现整体换成 Cypress 维护的 fork,而不是继续修一个死项目。同样的手法用在 node-domexception 上,目标包是 @nolyfill/domexception。
node-domexception 还有个细节:它先被钉成 npm:@nolyfill/domexception@^1.0.28(带浮动范围),覆盖 50 个版本(2026-03-02 → 04-21),之后才收紧成 @1.0.28 精确版,覆盖后 62 个版本(04-22 → 05-10)。同一个 override 目标,先浮动后收紧,这条改动在 git 历史之外没有任何地方说明过原因。
第三种写法出现在顶层 overrides 里:

"@aws-sdk/client-bedrock-runtime": "$@aws-sdk/client-bedrock-runtime" 是 self-reference 语法,意思是「和 dependencies 里声明的版本保持一致」。它的作用是:让 overrides 能显式覆盖掉某个 pnpm.overrides 已经管过的包,而不必去猜具体版本号。
2.2 🔴 关键发现:两套 overrides 在 96 个版本里内容从不一致
package.json 支持两种 overrides:pnpm.overrides(pnpm 专用)和顶层 overrides(npm 通用)。OpenClaw 在 96 个版本里同时写了这两个块,时间从 2026.1.29-beta.1 到 2026.5.10-beta.2。
逐版本比对这 96 个版本的两块内容,结果是:0 个版本内容完全相同,96 个全部不同。
在最后一个双块版本 2026.5.10-beta.2 上:
pnpm.overrides有 25 个条目(22 个普通 + 3 个别名/自引用)- 顶层
overrides只有 7 个条目 - 顶层有的 7 个键,全部也是
pnpm.overrides的键(无独有键) - 6 个键两边取值相同(如
axios: 1.16.0、uuid: 14.0.0、fast-uri: 3.1.2) - 1 个键取值冲突:
@aws-sdk/client-bedrock-runtime在 pnpm 侧是3.1045.0,在顶层是$@aws-sdk/client-bedrock-runtime - 18 个键只存在于
pnpm.overrides(qs、tar、hono、protobufjs等)
也就是说,顶层 overrides 是 pnpm overrides 的一个更小的子集,且对同一个包用了不同的钉法。这带来一个直接后果:
用 npm 装 OpenClaw,拿到的是 7 条覆盖规则;用 pnpm 装,拿到的是 25 条。换包管理器会换掉 16 个传递依赖的实际版本。
这不是 bug——$ 自引用在 npm 和 pnpm 下的语义解析路径不同,官方文档也建议只写一套。但 registry 元数据里没有任何字段提示用户这件事,而用户默认会用 npm。
三、13 小时锁定真空:撤补丁和补精确版本不是同一件事
把 2026-05-08 到 2026-05-14 的版本逐个铺开,能看到一段非常密集的连续改动:
| 版本 | 发布时间(UTC) | pnpm.ov | top.ov | deps | 带 ^ 的依赖数 |
packageManager |
|---|---|---|---|---|---|---|
2026.5.9-beta.1 |
05-09 13:27 | 25 | 7 | 61 | 37 | pnpm@10.33.2 |
2026.5.10-beta.1 |
05-10 13:59 | 25 | 7 | 61 | 37 | pnpm@10.33.2 |
2026.5.10-beta.2 |
05-10 18:15 | 25 | 7 | 61 | 37 | pnpm@10.33.2 |
2026.5.10-beta.3 |
05-11 03:28 | 0 | 7 | 61 | 37 | pnpm@11.0.8 |
2026.5.10-beta.4 |
05-11 16:03 | 0 | 7 | 61 | 38 | pnpm@11.0.8 |
2026.5.10-beta.5 |
05-11 16:38 | 0 | 7 | 62 | 39 | pnpm@11.0.8 |
2026.5.12-beta.1 |
05-12 17:09 | 0 | 7 | 62 | 0 | pnpm@11.1.0 |
看最后两列的交叉点:
2026-05-11 03:28 到 16:38,共 13.2 小时、3 个版本,这三个版本同时满足两个条件——pnpm.overrides 已被删除,而 dependencies 里的 ^ 浮动范围仍有 37~39 个。这是全时间线里唯一一段「传递依赖既不被 overrides 钉死、也不被精确版本约束」的窗口。全量扫描确认,符合这个条件的版本恰好就是这 3 个,再无第四个。
到 2026.5.12-beta.1(05-12 17:09),dependencies 里的 39 个 ^ 一次性归零,packageManager 同时跳到 pnpm 11.1.0。此后 122 个连续版本、全部 65 个直接依赖、全部 79 个 optionalDependencies,无一条浮动范围。
所以真相是:这套钉死机制不是被「删除」,而是被「替换」了。 旧方案是 overrides 钉传递依赖 + 直接依赖带浮动范围;新方案是所有直接依赖锁死到三位数精确版本。两者目标相同——确定性安装——手段完全不同。
值得留意的是替换过程中的一次反向回退:2026.5.10-beta.4 的带 ^ 依赖数从 37 涨到 38,.beta.5 又涨到 39。在整体收紧的窗口里出现依赖数增长,说明当时正在增删依赖,还没走到收紧那一步。
packageManager 字段本身也经历了 7 次变更:
| 生效版本 | pnpm 版本 | 距上一个变更 |
|---|---|---|
2026.1.29-beta.1 |
10.23.0 | — |
2026.3.24-beta.1 |
10.32.1 | 54 天 |
2026.4.19-beta.1 |
10.33.0 | 24 天 |
2026.4.29-beta.1 |
10.33.2+sha512 | 11 天 |
2026.5.10-beta.3 |
11.0.8+sha512 | 10 天 |
2026.5.12-beta.1 |
11.1.0+sha512 | 1 天 |
2026.5.22-beta.1 |
11.2.2+sha512 | 10 天 |
主版本号 10 → 11 的跳跃恰好发生在 overrides 被删的那一个版本上。两者是同一次改动里的两个动作,还是先后无关的两次变更,registry 元数据没有任何字段能区分,此处记为 UNKNOWN。
四、今天的 OpenClaw 到底还剩什么
把当前最新稳定版 2026.10.4(2026-10-15T23:58:28.695Z 发布)的元数据摊开,钉死层是彻底空的:
| 字段 | 2026.10.4 状态 |
|---|---|
pnpm |
ABSENT(最后一次出现:2026.5.10-beta.2,158 天前) |
overrides |
ABSENT(最后一次:2026.6.2-beta.1,134 天前) |
packageManager |
ABSENT(最后一次:2026.7.1-2,89 天前) |
onlyBuiltDependencies |
ABSENT(最后一次:2026.5.10-beta.2) |
dependencies |
65 条,全部精确版本,零 ^ 零 ~ |
optionalDependencies |
79 条,全部精确版本 |
devDependencies |
62 条,全部精确版本 |
bundleDependencies |
3 条:@openclaw/proxyline、chrome-devtools-mcp、undici |
peerDependencies |
ABSENT(历史上出现过 93 个版本后归零) |
下面这张图是 2026.10.4 的 package.json 在同一个滚动位置上的样子——对照上面 2026.5.10-beta.2 的截图,optionalDependencies 后面没有 overrides,engines 后面没有 packageManager:

4.1 那 16 个传递依赖现在归谁管
这是切换方案最实际的一个后果,也是这套分析里最需要读者自己判断的地方。
2026.5.10-beta.2 时期,qs、axios、form-data、uuid、protobufjs、tough-cookie、yauzl 等 16 个传递依赖的版本由 OpenClaw 的 overrides 单方面决定。今天这些 overrides 不存在了,它们的实际版本改为由上游包各自的 semver 声明决定——也就是由 hono、express 系包、各类 SDK 的依赖树决定。
从理论上说,这降低了维护成本(上游发新版能自动跟上)。但代价同样明确:OpenClaw 不再有能力在某个传递依赖出现安全问题或行为回归时,用一次 overrides 改动把全站用户挡在门外。这个取舍是有意的还是被迫的,本文不做推断,记为 UNKNOWN。
可以确定的是:当前所有直接依赖都是精确版本,安装结果是可复现的。这一点上今天的 OpenClaw 比 5 月之前更严格,不是更松。
4.2 一个需要修正的直觉
常见的说法是「npm 包有 lockfile,所以传递依赖被锁住了」。对项目成立,对发布的库不成立。
registry 元数据里的 _hasShrinkwrap 字段可以佐证:278 个版本里只有 62 个为 True,且这 62 个极度碎片化——2026-08-10、2026-09-11、2026-09-20、2026-09-23 各出现一次立刻又变回 False。2026.10.4 的值是 False。
也就是说,OpenClaw 这个发布包绝大多数时候不携带 shrinkwrap。npm 的 shrinkwrap 本来也是给应用(application)用的,对库(library)会覆盖下游的依赖树判断,很少有库这么做。所以下游装 OpenClaw 时,传递依赖的实际解析结果由下游自己的 lockfile 决定,OpenClaw 侧无法约束。
这也反过来解释了为什么当年要引入 overrides:在一个不携带 shrinkwrap 的发布包里,这是唯一能约束传递依赖版本的手段。
五、给使用者的三条可操作结论
一、你现在装到的依赖树,与 5 月之前的 OpenClaw 不是同一棵树。 钉死的 16 个传递依赖现在由上游决定,两次安装同一版本 OpenClaw,如果 lockfile 之外的上游发过新版,实际拿到的传递依赖可能不同。
二、选包管理器会影响历史版本的结果,但不影响今天。 2026.5.10-beta.2 那个时期,npm 和 pnpm 拿到的覆盖规则是 7 条 vs 25 条。今天三个钉死字段全部为空,两个包管理器看到的元数据一致,这条差异已经消失。
三、要验证而不是相信。 直接打开 unpkg 上的 2026.5.10-beta.2 package.json 和 2026.10.4 的对应文件,搜 overrides 和 packageManager 两个键,就能自己确认本文的时间线。
六、证据口径与已知未知
本文所有数字的采集与计算方式:
- 主数据源:
https://registry.npmjs.org/openclaw,2026-10-18 06:0x 采集,响应体 19,886,890 字节,time.modified为2026-10-15T23:58:29.036Z。278 个版本全量解析,非抽样。 - 交叉核对源:unpkg CDN 上
2026.5.10-beta.2(99,397 字节)与2026.10.4(148,587 字节)两个版本的package.json原文,与 registry 元数据逐字段一致。 - 截图:三张均为 unpkg 页面的真实渲染截图,Chromium 无头模式 1280px 视口、DPR=2,定位到目标键所在行后逐张目视确认内容非 404、非空白、非挑战页。
必须声明的未知项:
- UNKNOWN:pnpm 10 → 11 的主版本跳跃与 overrides 删除是否属于同一次改动,registry 无字段可判定。
- UNKNOWN:
node-domexception的 override 先浮动后收紧的动机,官方无说明。 - UNKNOWN:2026-06-03 之后顶层 overrides、2026-07-18 之后 packageManager 分别移除的决策原因。
- UNKNOWN:全部结论均基于包元数据的声明,未执行任何安装、未跑依赖审计工具、未做供应链溯源。
- 口径:本文的「传递依赖」= 在该版本的
dependencies中不存在、但出现在pnpm.overrides中的条目。该定义基于包元数据,不等于运行时实际依赖树的完整拓扑。 - 口径:
^计数按字符串包含^统计,未区分^与^=等边界情况;全量扫描中未出现此类写法。
七、与站内其他 OpenClaw 内容的分工
| 文章 | 切入层 | 与本文的边界 |
|---|---|---|
| 《OpenClaw npm 278 版本台账:Node 引擎区间在 9 月 8 日整体换掉》 | registry 元数据的版本与安装契约层 | 该文覆盖 dist-tags、engines、依赖浮动范围的总体时间线;本文只深挖 pnpm/overrides/packageManager 这三个安装期钉死字段的内部结构与生命周期,两者数据源相同、切入字段不重叠。 |
| 《OpenClaw 安装体积台账:LTS 371.9 MiB vs latest 245.9 MiB》 | registry 元数据的交付物形态层 | 该文关注 unpackedSize / fileCount / worker tarball;本文关注依赖版本如何被约束,不涉及体积。 |
| 《OpenClaw 模块面:346 条 exports 子路径与 38.4% 的消失率》 | registry 元数据的模块解析层 | 该文关注 exports 字段;本文关注的三个字段均不参与模块解析。 |
| 《OpenClaw GHSA 100 条安全公告授权审计》 | 官方安全公告层 | 该文基于 GHSA 一手公告;本文的「哪些包值得钉」是元数据层面的推断,未对照任何 CVE 编号,因此两文的安全结论不可互相印证。 |
八、来源
- OpenClaw npm registry 完整元数据 —— 278 个版本全量解析,2026-10-18 06:0x 采集,19,886,890 字节
- unpkg:openclaw@2026.5.10-beta.2/package.json —— 钉死层完整原文,99,397 字节
- unpkg:openclaw@2026.10.4/package.json —— 钉死层缺席原文,148,587 字节
- pnpm overrides 官方文档 ——
npm:别名与$自引用语法语义 - npm 文档:overrides —— 顶层 overrides 与 pnpm overrides 的作用域差异
本文所有统计均由脚本对 registry 全量元数据计算得出,标题中的每个数字都有对应的正文表格或段落支撑。未执行安装、未调用任何需要认证的端点、未访问官方仓库的内部提交记录。








