Skip to content

[Bug]: "Unsafe package dist path" error from post-install runtime-deps staging (self-collision) on user-writable global installs (Homebrew Node on macOS) #71261

@aaajiao

Description

@aaajiao

Summary

On a user-writable global openclaw install (macOS + Homebrew-provided Node, no sudo required because Homebrew's lib/node_modules/ is user-owned), openclaw update fails after the npm install -g step completes successfully — the post-install bundled plugin runtime-deps copy phase creates a transient staging directory whose path the same install flow's own safety check then rejects as an "Unsafe package dist path".

aaajiao@aaajiao-M4-Max-16 ~> openclaw update
Updating OpenClaw...
│
◇  ✓ Updating via package manager (15.52s)
Error: Unsafe package dist path: dist/extensions/amazon-bedrock/.openclaw-runtime-deps-copy-KZmXaz/node_modules/.bin/fxparser

The leading . on the staging directory name (.openclaw-runtime-deps-copy-<random>) is what makes the check bail. The staging dir is self-cleaned on error (confirmed by find returning no such directory after the failure), which makes this purely self-induced: openclaw's own post-install creates a path that openclaw's own subsequent safety check immediately rejects.

Behavior

  1. npm install -g openclaw@<version> completes (CLI binary + dist/ tree are now at the new version — openclaw --version reports the new number).
  2. openclaw update's post-install phase walks bundled plugins and prepares their runtime deps. For each bundled plugin with runtime deps it:
    • Creates dist/extensions/<plugin>/.openclaw-runtime-deps-copy-<6-char-random>/
    • Populates it with the plugin's runtime deps (including npm-created bin symlinks such as .bin/fxparser for fast-xml-parser)
    • Intends to atomically rename/move it into dist/extensions/<plugin>/node_modules/
  3. Before the rename completes, the safety check walks dist/extensions/ and flags the .openclaw-runtime-deps-copy-* path as unsafe (.-prefixed names are rejected).
  4. Install fails with the error above. Staging directory is cleaned up as part of the error path.
  5. Result: the dist/ tree is at the new version but the bundled plugin whose staging collided (e.g. amazon-bedrock) may have partially-populated node_modules/ or the previous version's node_modules/.

Subsequent openclaw update invocations report:

Update Result: SKIPPED
  Root: /opt/homebrew/lib/node_modules/openclaw
  Reason: already-current
  Before: 2026.4.23
  After: 2026.4.23

Total time: 0ms

because step 1 already brought the CLI version up, so the next update has nothing to do and never re-attempts the post-install phase. The user is left with an install that appears current but skipped the runtime-deps finalization for the colliding plugin. The problem re-triggers on the next real version bump.

Evidence that the staging dir is self-created and self-cleaned

After the error fires, nothing is left on disk under the install tree matching the staging name:

$ find /opt/homebrew/lib/node_modules/openclaw/dist/extensions -maxdepth 2 -type d -name ".openclaw-runtime-deps-copy-*"
# (no output)

$ find /opt/homebrew/lib/node_modules -type d -name ".openclaw-runtime-deps-copy-*"
# (no output)

$ find /tmp /var/folders ~/.openclaw -type d -name ".openclaw-runtime-deps-copy-*" 2>/dev/null
# (no output)

And dist/extensions/amazon-bedrock/ already has its node_modules/ (sibling to the source files), so the install layout on user-writable installs is runtime deps live in-tree under dist/extensions/<plugin>/node_modules/ — not in ~/.openclaw/plugin-runtime-deps/ like on root-owned global installs:

$ ls /opt/homebrew/lib/node_modules/openclaw/dist/extensions/amazon-bedrock/
api.js
config-compat.js
discovery.js
embedding-provider.js
index.js
memory-embedding-adapter.js
node_modules
openclaw.plugin.json
package.json
register.sync.runtime.js
setup-api.js

$ ls ~/.openclaw/plugin-runtime-deps/
(does not exist — this path is only used by root-owned global installs)

So the .openclaw-runtime-deps-copy-<random>/ directory is a transient atomicity-copy target internal to the post-install, not a leftover artifact or persistent staging.

Reproduction

  • Clean macOS arm64 host with Homebrew-managed Node (Homebrew's /opt/homebrew/lib/node_modules/ is user-owned, so global npm install -g openclaw does not require sudo)
  • Install openclaw globally: brew install node then npm install -g openclaw@<prev-version> (any version before 2026.4.23)
  • Run openclaw update to bump to 2026.4.23

Expected: CLI + all bundled plugin runtime deps updated cleanly.

Actual: CLI updates, but the post-install phase aborts with Error: Unsafe package dist path: dist/extensions/amazon-bedrock/.openclaw-runtime-deps-copy-<random>/node_modules/.bin/fxparser.

Subsequent openclaw update runs report already-current and skip, masking the partial state.

Suggested upstream fixes

Three non-exclusive options:

  1. Change staging name to not start with . — use e.g. _staging-runtime-deps-<random>/ or a single well-known bucket like dist/extensions/<plugin>/_staging/<random>/. Keeps staging in-tree for locality but avoids the "unsafe dotfile-prefixed dir" rule.
  2. Move staging out of dist/ entirely — create under $TMPDIR (mkdtemp()), populate there, then rename() into dist/extensions/<plugin>/node_modules/. If the target is on the same filesystem as $TMPDIR, rename stays atomic. Benefits: safety check never has to know about staging existence.
  3. Whitelist the openclaw-owned staging prefix in the safety check — if the intent of rejecting .-prefixed names in dist/ is to block arbitrary foreign hidden directories, explicitly allow the single pattern ^\.openclaw-runtime-deps-copy-[A-Za-z0-9]+$ that openclaw's own post-install creates.

Option (2) is the most robust (staging physically cannot collide with safety check because it's not in dist/). Option (3) is the smallest diff.

Environment

  • OpenClaw: 2026.4.23
  • Node: Homebrew-installed (Homebrew node formula as of 2026-04-24)
  • npm: bundled with Homebrew node
  • OS: macOS, arm64 (Apple Silicon)
  • Install path: /opt/homebrew/lib/node_modules/openclaw (user-owned via Homebrew Node)
  • openclaw binary: /opt/homebrew/bin/openclaw → symlink to ../lib/node_modules/openclaw/openclaw.mjs
  • Install method: npm install -g openclaw@<version> (no sudo; Homebrew's lib tree is user-writable)

Why this may be less visible in other install topologies

  • Root-owned global installs (Linux + sudo npm install -g, typical Docker image): post-install instead writes runtime deps into ~/.openclaw/plugin-runtime-deps/openclaw-<ver>-<hash>/ (external to the root-owned install tree), so dist/extensions/<plugin>/ is never touched by the copy phase and the safety check never sees the staging dir there. Confirmed by checking a Linux VM install on 2026.4.23: dist/extensions/<plugin>/ has no node_modules/, and ~/.openclaw/plugin-runtime-deps/openclaw-2026.4.23-<hash>/ holds the full dep tree.
  • User-writable global installs (Homebrew Node on macOS, nvm-managed Node on Linux, pnpm install -g in user space): post-install writes in-tree under dist/extensions/<plugin>/node_modules/, which is where the staging collision happens.

So this bug is specifically visible on user-writable installs. Anyone using Homebrew Node on macOS or nvm/pnpm on Linux will hit it on every real version bump.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions