openclaw - 💡(How to fix) Fix [Bug]: openclaw update fails at "global install swap" step when npm hardlinks dependency binaries (macOS + fnm)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

openclaw update reliably fails on macOS with the npm installer + fnm-managed Node when any dependency in the staging install tree contains npm-hardlinked binaries — most notably esbuild. The global install swap step aborts with Hardlinked source file is not allowed: .../node_modules/esbuild/bin/esbuild, openclaw rolls back, and before.version === after.version. Every subsequent openclaw update re-fails the same way, locking the host on the old version while ai.openclaw.gateway LaunchAgent gets stop+restarted on each attempt (so the gateway flaps without actually upgrading).

Related but distinct from #79209 (Workspace bootstrap files reported [MISSING] when hardlinked) — that issue covers the bootstrap file read path of the same nlink > 1 guard family. This issue is the update swap write path. Same mechanism (rejectHardlinks default true on the openBoundaryFile family), different code path, different remediation.

Also touches #77511 (Move package update execution into an external updater) — once the in-process atomic-swap step is moved to an external updater, this whole class of issues likely disappears at the source.

Error Message

"status": "error", "command": "<fnm>/installation/bin/npm i -g --prefix <fnm>/installation/lib/node_modules/.openclaw-update-stage-2scbML openclaw@latest --no-fund --no-audit --loglevel=error",

Root Cause

openclaw update reliably fails on macOS with the npm installer + fnm-managed Node when any dependency in the staging install tree contains npm-hardlinked binaries — most notably esbuild. The global install swap step aborts with Hardlinked source file is not allowed: .../node_modules/esbuild/bin/esbuild, openclaw rolls back, and before.version === after.version. Every subsequent openclaw update re-fails the same way, locking the host on the old version while ai.openclaw.gateway LaunchAgent gets stop+restarted on each attempt (so the gateway flaps without actually upgrading).

Related but distinct from #79209 (Workspace bootstrap files reported [MISSING] when hardlinked) — that issue covers the bootstrap file read path of the same nlink > 1 guard family. This issue is the update swap write path. Same mechanism (rejectHardlinks default true on the openBoundaryFile family), different code path, different remediation.

Also touches #77511 (Move package update execution into an external updater) — once the in-process atomic-swap step is moved to an external updater, this whole class of issues likely disappears at the source.

Fix Action

Fix / Workaround

Reproduced three times in a row from the same host before applying any workaround.

Workaround (currently in use)

Result after workaround

Code Example

npm install -g openclaw@2026.5.12

---

openclaw update --yes --json

---

{
  "status": "error",
  "mode": "npm",
  "root": "<fnm>/installation/lib/node_modules/openclaw",
  "reason": "global install swap",
  "before": { "version": "2026.5.12" },
  "after":  { "version": "2026.5.12" },
  "steps": [
    {
      "name": "global update",
      "command": "<fnm>/installation/bin/npm i -g --prefix <fnm>/installation/lib/node_modules/.openclaw-update-stage-2scbML openclaw@latest --no-fund --no-audit --loglevel=error",
      "durationMs": 34659,
      "exitCode": 0,
      "stdoutTail": "\nadded 483 packages in 35s",
      "stderrTail": null
    },
    {
      "name": "global install swap",
      "command": "swap <stage>/lib/node_modules/openclaw -> <fnm>/installation/lib/node_modules/openclaw",
      "durationMs": 218,
      "exitCode": 1,
      "stdoutTail": null,
      "stderrTail": "Hardlinked source file is not allowed: <fnm>/installation/lib/node_modules/openclaw/node_modules/esbuild/bin/esbuild"
    }
  ],
  "durationMs": 39669
}

---

npm install -g openclaw@latest --no-fund --no-audit
launchctl kickstart -k gui/$(id -u)/ai.openclaw.gateway
launchctl kickstart -k gui/$(id -u)/ai.openclaw.node
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

openclaw update reliably fails on macOS with the npm installer + fnm-managed Node when any dependency in the staging install tree contains npm-hardlinked binaries — most notably esbuild. The global install swap step aborts with Hardlinked source file is not allowed: .../node_modules/esbuild/bin/esbuild, openclaw rolls back, and before.version === after.version. Every subsequent openclaw update re-fails the same way, locking the host on the old version while ai.openclaw.gateway LaunchAgent gets stop+restarted on each attempt (so the gateway flaps without actually upgrading).

Related but distinct from #79209 (Workspace bootstrap files reported [MISSING] when hardlinked) — that issue covers the bootstrap file read path of the same nlink > 1 guard family. This issue is the update swap write path. Same mechanism (rejectHardlinks default true on the openBoundaryFile family), different code path, different remediation.

Also touches #77511 (Move package update execution into an external updater) — once the in-process atomic-swap step is moved to an external updater, this whole class of issues likely disappears at the source.

Steps to reproduce

  1. Install OpenClaw globally on macOS with fnm-managed Node (reproduced on v24.13.0):
    npm install -g [email protected]
  2. Run a normal update against the latest stable:
    openclaw update --yes --json
  3. Observe:
    • npm staging install completes (exitCode: 0, ~35s, ~480 packages added to .openclaw-update-stage-XXXXX)
    • global install swap fails with Hardlinked source file is not allowed: .../node_modules/esbuild/bin/esbuild
    • openclaw rolls back; before.version === after.version === "2026.5.12"
    • LaunchAgent ai.openclaw.gateway gets stopped + restarted but ends up running the old binary again

Actual behavior

Full JSON output from openclaw update --yes --json (paths trimmed for readability):

{
  "status": "error",
  "mode": "npm",
  "root": "<fnm>/installation/lib/node_modules/openclaw",
  "reason": "global install swap",
  "before": { "version": "2026.5.12" },
  "after":  { "version": "2026.5.12" },
  "steps": [
    {
      "name": "global update",
      "command": "<fnm>/installation/bin/npm i -g --prefix <fnm>/installation/lib/node_modules/.openclaw-update-stage-2scbML openclaw@latest --no-fund --no-audit --loglevel=error",
      "durationMs": 34659,
      "exitCode": 0,
      "stdoutTail": "\nadded 483 packages in 35s",
      "stderrTail": null
    },
    {
      "name": "global install swap",
      "command": "swap <stage>/lib/node_modules/openclaw -> <fnm>/installation/lib/node_modules/openclaw",
      "durationMs": 218,
      "exitCode": 1,
      "stdoutTail": null,
      "stderrTail": "Hardlinked source file is not allowed: <fnm>/installation/lib/node_modules/openclaw/node_modules/esbuild/bin/esbuild"
    }
  ],
  "durationMs": 39669
}

Reproduced three times in a row from the same host before applying any workaround.

Expected behavior

Either:

  • The global install swap step should accept hardlinked source files. An st_nlink > 1 value on a file we just created inside a staging directory we own does not imply TOCTOU or privilege-escalation risk — the destination has the same uid/gid and lives inside the install tree the swap step just produced.
  • Or the swap step should fs.unlink + fs.copyFile (or use a clone-on-write path on APFS) when it encounters a hardlinked source, instead of refusing the operation outright.

The --prefer-copy npm flag (which would make npm skip its hardlink-from-cache optimization) is not currently exposed through openclaw update and would only paper over the npm cache → staging path — the underlying guard policy would still reject any other hardlinked file in the tree.

Why this now reproduces 100%

npm uses hardlinks aggressively when the npm cache (~/.npm/_cacache) is on the same filesystem as the install target — the default on macOS. esbuild ships its native binary under @esbuild/<platform>-<arch>/bin/esbuild, and node_modules/esbuild/bin/esbuild ends up hardlinked in this layout. As of 2026.5.x esbuild appears unavoidable in the OpenClaw dependency tree (control-ui / build chain), so any user with fnm + macOS + standard npm config will reliably hit this on every openclaw update.

Workaround (currently in use)

Bypass openclaw's atomic-swap wrapper, let npm overwrite in place, then re-restart both LaunchAgents:

npm install -g openclaw@latest --no-fund --no-audit
launchctl kickstart -k gui/$(id -u)/ai.openclaw.gateway
launchctl kickstart -k gui/$(id -u)/ai.openclaw.node

This loses: plugin sync, completion cache refresh, doctor checks, and the structured update.run JSON contract — they have to be run manually. It is an emergency path, not a sustainable upgrade route.

Result after workaround

  • binary upgraded 2026.5.12 → 2026.5.20 cleanly
  • Host OpenClaw 2026.5.20 >= 2026.3.22, OK. compat check passes
  • protocol mismatch warnings (gateway expectedProtocol: 4 vs old node host minProtocol: 3, maxProtocol: 3, version: "2026.5.7") stopped within seconds — the host had accumulated 54,345 such warnings while the in-process update kept stop+restarting the gateway without actually upgrading the node host.

OpenClaw version

Environment

  • macOS 26.4.1 (arm64) on Apple Silicon
  • Node.js v24.13.0, installed via fnm at ~/.local/share/fnm/node-versions/v24.13.0
  • npm bundled with that fnm Node version
  • esbuild in dependency tree at node_modules/esbuild/bin/esbuild, hardlinked by npm during install (nlink=2, shared with ~/.npm/_cacache)
  • fnm version: standard fnm install, no per-shell pinning

Related issues

  • #79209 — same nlink > 1 guard family, different application point (bootstrap file read path). Closed without an official PR (workaround was a user-applied sed patch on dist/safe-*.js bundles). The guard is still active in 2026.5.20, just exposed via a different code path now.
  • #77511 — maintainer-proposed external updater that would obsolete the in-process swap step.
  • #64892 — Agent self-update bypasses update.run and uses raw npm global upgrade without restarting gateway. Related concern: the bare-npm install -g fallback we are currently forced into is undertested as an upgrade path.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Either:

  • The global install swap step should accept hardlinked source files. An st_nlink > 1 value on a file we just created inside a staging directory we own does not imply TOCTOU or privilege-escalation risk — the destination has the same uid/gid and lives inside the install tree the swap step just produced.
  • Or the swap step should fs.unlink + fs.copyFile (or use a clone-on-write path on APFS) when it encounters a hardlinked source, instead of refusing the operation outright.

The --prefer-copy npm flag (which would make npm skip its hardlink-from-cache optimization) is not currently exposed through openclaw update and would only paper over the npm cache → staging path — the underlying guard policy would still reject any other hardlinked file in the tree.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Bug]: openclaw update fails at "global install swap" step when npm hardlinks dependency binaries (macOS + fnm)