openclaw - 💡(How to fix) Fix `openclaw doctor --fix` reports success but bundled plugin deps land outside the bucket → infinite "missing" loop [1 comments, 2 participants]

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…
GitHub stats
openclaw/openclaw#72779Fetched 2026-04-28 06:32:17
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
0
Timeline (top)
closed ×1commented ×1

On v2026.4.24 (latest at time of writing), openclaw doctor --fix for the Bundled plugin runtime deps check declares "Installed bundled plugin deps: …" but the bucket's node_modules stays empty. The next doctor invocation re-detects them as missing → user gets stuck in an install loop.

The root cause is in installBundledRuntimeDeps() (dist/bundled-runtime-deps-*.js): when installExecutionRoot === installRoot (the non-isolated path taken by repairBundledRuntimeDepsInstallRoot()), no package.json is written in the install root before npm install <specs> runs. With no package.json anchor, npm walks up the tree and lands the dependencies in the nearest ancestor's node_modules (in our case the openclaw CLI's own node_modules at ~/.npm-global/lib/node_modules/openclaw/node_modules/) — not in the bucket.

After install, the doctor note(...) only echoes back the spec list it asked for; it does not verify any package landed in the bucket's node_modules. So the user sees a green "Installed bundled plugin deps: …" message even though nothing was installed where the loader looks.

The result: plugin runtime loading silently fails, and the gateway ends up with ready (0 plugins, …) after every restart.

Error Message

if (!fs.existsSync(stagedNodeModulesDir)) throw new Error("npm install did not produce node_modules"); // ← gated on isolated 2. Always verify <installRoot>/node_modules was populated after spawnSync returns 0 — both in the isolated and non-isolated paths. If empty/missing, throw so doctor can surface a real error instead of a fake green note.

  •  if (!fs.existsSync(stagedNodeModulesDir)) throw new Error("npm install did not produce node_modules");
  • if (!fs.existsSync(stagedNodeModulesDir)) throw new Error("npm install did not produce node_modules");

Root Cause

  • Affects every fresh install and every upgrade that produces a new bucket hash.
  • Symptom (TUI looks broken, channels won't load, ready (0 plugins, …)) gives no visible hint that the cause is in the bundled-deps install path. Users naturally suspect VPN / network / config issues first.
  • The doctor loop is hard to escape without reading source — it actively misleads by reporting success.

Happy to send a PR if it would help; let me know if you'd prefer a different shape than the patch sketched above.

Fix Action

Fix / Workaround

Patch sketch:

Workaround (for affected users)

Happy to send a PR if it would help; let me know if you'd prefer a different shape than the patch sketched above.

Code Example

ls -la ~/.openclaw/plugin-runtime-deps/openclaw-2026.4.24-*/node_modules
# 0 entries

# But:
ls ~/.npm-global/lib/node_modules/openclaw/node_modules/@mariozechner/pi-ai/package.json
# exists — install landed in the CLI's own node_modules instead

---

function installBundledRuntimeDeps(params) {
  const installExecutionRoot = params.installExecutionRoot ?? params.installRoot;
  const isolatedExecutionRoot = path.resolve(installExecutionRoot) !== path.resolve(params.installRoot);
  // ...
  fs.mkdirSync(params.installRoot, { recursive: true });
  fs.mkdirSync(installExecutionRoot, { recursive: true });
  if (isolatedExecutionRoot) fs.writeFileSync(  //  ← gated on isolated
    path.join(installExecutionRoot, "package.json"),
    JSON.stringify({ name: "openclaw-runtime-deps-install", private: true }, null, 2) + "\n",
    "utf8"
  );
  // ... spawnSync npm install ...
  if (isolatedExecutionRoot) {
    const stagedNodeModulesDir = path.join(installExecutionRoot, "node_modules");
    if (!fs.existsSync(stagedNodeModulesDir)) throw new Error("npm install did not produce node_modules");  //  ← gated on isolated
    replaceNodeModulesDir(path.join(params.installRoot, "node_modules"), stagedNodeModulesDir);
  }
}

---

note(`Installed bundled plugin deps: ${repairBundledRuntimeDepsInstallRoot({...}).installSpecs.join(", ")}`, "Bundled plugins");

---

-    if (isolatedExecutionRoot) fs.writeFileSync(path.join(installExecutionRoot, "package.json"), `${JSON.stringify({
-      name: "openclaw-runtime-deps-install",
-      private: true
-    }, null, 2)}\n`, "utf8");
+    fs.writeFileSync(path.join(installExecutionRoot, "package.json"), `${JSON.stringify({
+      name: "openclaw-runtime-deps-install",
+      private: true
+    }, null, 2)}\n`, "utf8");

---

-    if (isolatedExecutionRoot) {
-      const stagedNodeModulesDir = path.join(installExecutionRoot, "node_modules");
-      if (!fs.existsSync(stagedNodeModulesDir)) throw new Error("npm install did not produce node_modules");
-      replaceNodeModulesDir(path.join(params.installRoot, "node_modules"), stagedNodeModulesDir);
-    }
+    const stagedNodeModulesDir = path.join(installExecutionRoot, "node_modules");
+    if (!fs.existsSync(stagedNodeModulesDir)) throw new Error("npm install did not produce node_modules");
+    if (isolatedExecutionRoot) {
+      replaceNodeModulesDir(path.join(params.installRoot, "node_modules"), stagedNodeModulesDir);
+    }

---

BUCKET=~/.openclaw/plugin-runtime-deps/openclaw-2026.4.24-XXXXXXXXXXXX/
cd "$BUCKET"
node -e '
  const fs = require("fs");
  const obj = JSON.parse(fs.readFileSync(".openclaw-runtime-deps.json","utf8"));
  const deps = {};
  for (const s of obj.specs) {
    const idx = s.lastIndexOf("@");
    deps[s.slice(0, idx)] = s.slice(idx + 1);
  }
  fs.writeFileSync("package.json", JSON.stringify({
    name: "openclaw-runtime-deps-bucket",
    private: true,
    dependencies: deps,
  }, null, 2) + "\n");
'
npm install --ignore-scripts --legacy-peer-deps --no-package-lock --no-fund --no-audit
RAW_BUFFERClick to expand / collapse

openclaw doctor --fix reports success but bundled plugin deps land outside the bucket → infinite "missing" loop

Summary

On v2026.4.24 (latest at time of writing), openclaw doctor --fix for the Bundled plugin runtime deps check declares "Installed bundled plugin deps: …" but the bucket's node_modules stays empty. The next doctor invocation re-detects them as missing → user gets stuck in an install loop.

The root cause is in installBundledRuntimeDeps() (dist/bundled-runtime-deps-*.js): when installExecutionRoot === installRoot (the non-isolated path taken by repairBundledRuntimeDepsInstallRoot()), no package.json is written in the install root before npm install <specs> runs. With no package.json anchor, npm walks up the tree and lands the dependencies in the nearest ancestor's node_modules (in our case the openclaw CLI's own node_modules at ~/.npm-global/lib/node_modules/openclaw/node_modules/) — not in the bucket.

After install, the doctor note(...) only echoes back the spec list it asked for; it does not verify any package landed in the bucket's node_modules. So the user sees a green "Installed bundled plugin deps: …" message even though nothing was installed where the loader looks.

The result: plugin runtime loading silently fails, and the gateway ends up with ready (0 plugins, …) after every restart.

Repro

  1. Install openclaw 2026.4.24 fresh, or trigger any state that causes ~/.openclaw/plugin-runtime-deps/<bucket>/node_modules/ to be empty while <bucket>/.openclaw-runtime-deps.json lists specs.
  2. openclaw doctor
  3. See "Bundled plugin runtime deps are missing" listing 20+ packages.
  4. Choose Yes at the "Install missing bundled plugin runtime deps now?" prompt (or openclaw doctor --fix).
  5. See "Installed bundled plugin deps: …" success note.
  6. Doctor re-runs the scan in the same session → reports the same packages still missing → reprompts.
  7. Loop forever no matter how many times you say Yes.

Manual confirmation that nothing was installed in the bucket:

ls -la ~/.openclaw/plugin-runtime-deps/openclaw-2026.4.24-*/node_modules
# 0 entries

# But:
ls ~/.npm-global/lib/node_modules/openclaw/node_modules/@mariozechner/pi-ai/package.json
# exists — install landed in the CLI's own node_modules instead

Expected vs actual

Expected: npm install lands packages in <bucket>/node_modules/, doctor reflows clean on next scan.

Actual: packages land in ~/.npm-global/lib/node_modules/openclaw/node_modules/ (or wherever npm's upward package.json search finds an anchor), bucket stays empty, doctor loops.

Environment

  • openclaw 2026.4.24
  • macOS (Apple Silicon), node 25.8.1, npm 11.x
  • Install method: npm install -g openclaw

Root cause (with code references)

In bundled-runtime-deps-*.js, installBundledRuntimeDeps:

function installBundledRuntimeDeps(params) {
  const installExecutionRoot = params.installExecutionRoot ?? params.installRoot;
  const isolatedExecutionRoot = path.resolve(installExecutionRoot) !== path.resolve(params.installRoot);
  // ...
  fs.mkdirSync(params.installRoot, { recursive: true });
  fs.mkdirSync(installExecutionRoot, { recursive: true });
  if (isolatedExecutionRoot) fs.writeFileSync(  //  ← gated on isolated
    path.join(installExecutionRoot, "package.json"),
    JSON.stringify({ name: "openclaw-runtime-deps-install", private: true }, null, 2) + "\n",
    "utf8"
  );
  // ... spawnSync npm install ...
  if (isolatedExecutionRoot) {
    const stagedNodeModulesDir = path.join(installExecutionRoot, "node_modules");
    if (!fs.existsSync(stagedNodeModulesDir)) throw new Error("npm install did not produce node_modules");  //  ← gated on isolated
    replaceNodeModulesDir(path.join(params.installRoot, "node_modules"), stagedNodeModulesDir);
  }
}

repairBundledRuntimeDepsInstallRoot calls this without installExecutionRoot, so isolatedExecutionRoot === false and both safety nets are skipped:

  1. No package.json written → npm install searches upward, lands packages elsewhere.
  2. The "did npm install actually produce node_modules?" check is skipped → install is treated as success even when the bucket is empty.

Then doctor-bundled-plugin-runtime-deps-*.js runs:

note(`Installed bundled plugin deps: ${repairBundledRuntimeDepsInstallRoot({...}).installSpecs.join(", ")}`, "Bundled plugins");

This .installSpecs.join(", ") just echoes the spec list it tried to install — never reads the filesystem to confirm. False success.

Suggested fix

Two-part fix in installBundledRuntimeDeps:

  1. Always write a minimal package.json in installExecutionRoot (drop the if (isolatedExecutionRoot) gate around fs.writeFileSync). Without an anchor, npm's upward search produces non-deterministic install locations.
  2. Always verify <installRoot>/node_modules was populated after spawnSync returns 0 — both in the isolated and non-isolated paths. If empty/missing, throw so doctor can surface a real error instead of a fake green note.

Patch sketch:

-    if (isolatedExecutionRoot) fs.writeFileSync(path.join(installExecutionRoot, "package.json"), `${JSON.stringify({
-      name: "openclaw-runtime-deps-install",
-      private: true
-    }, null, 2)}\n`, "utf8");
+    fs.writeFileSync(path.join(installExecutionRoot, "package.json"), `${JSON.stringify({
+      name: "openclaw-runtime-deps-install",
+      private: true
+    }, null, 2)}\n`, "utf8");
-    if (isolatedExecutionRoot) {
-      const stagedNodeModulesDir = path.join(installExecutionRoot, "node_modules");
-      if (!fs.existsSync(stagedNodeModulesDir)) throw new Error("npm install did not produce node_modules");
-      replaceNodeModulesDir(path.join(params.installRoot, "node_modules"), stagedNodeModulesDir);
-    }
+    const stagedNodeModulesDir = path.join(installExecutionRoot, "node_modules");
+    if (!fs.existsSync(stagedNodeModulesDir)) throw new Error("npm install did not produce node_modules");
+    if (isolatedExecutionRoot) {
+      replaceNodeModulesDir(path.join(params.installRoot, "node_modules"), stagedNodeModulesDir);
+    }

Bonus: in doctor-bundled-plugin-runtime-deps, after repairBundledRuntimeDepsInstallRoot() returns, re-run scanBundledPluginRuntimeDeps() once more and only emit the success note if missing.length === 0. Today's blind echo lets a silent failure look identical to a successful install.

Workaround (for affected users)

Until a release ships, manually create the bucket's package.json from .openclaw-runtime-deps.json and run npm install in-place:

BUCKET=~/.openclaw/plugin-runtime-deps/openclaw-2026.4.24-XXXXXXXXXXXX/
cd "$BUCKET"
node -e '
  const fs = require("fs");
  const obj = JSON.parse(fs.readFileSync(".openclaw-runtime-deps.json","utf8"));
  const deps = {};
  for (const s of obj.specs) {
    const idx = s.lastIndexOf("@");
    deps[s.slice(0, idx)] = s.slice(idx + 1);
  }
  fs.writeFileSync("package.json", JSON.stringify({
    name: "openclaw-runtime-deps-bucket",
    private: true,
    dependencies: deps,
  }, null, 2) + "\n");
'
npm install --ignore-scripts --legacy-peer-deps --no-package-lock --no-fund --no-audit

Repeat for every bucket in ~/.openclaw/plugin-runtime-deps/. Then launchctl kickstart -k gui/$(id -u)/ai.openclaw.gateway (macOS) or equivalent restart on other platforms.

Why this matters

  • Affects every fresh install and every upgrade that produces a new bucket hash.
  • Symptom (TUI looks broken, channels won't load, ready (0 plugins, …)) gives no visible hint that the cause is in the bundled-deps install path. Users naturally suspect VPN / network / config issues first.
  • The doctor loop is hard to escape without reading source — it actively misleads by reporting success.

Happy to send a PR if it would help; let me know if you'd prefer a different shape than the patch sketched above.

extent analysis

TL;DR

The issue can be fixed by modifying the installBundledRuntimeDeps function to always write a minimal package.json in installExecutionRoot and verify that <installRoot>/node_modules was populated after spawnSync returns 0.

Guidance

  • Modify the installBundledRuntimeDeps function to remove the if (isolatedExecutionRoot) gate around fs.writeFileSync to ensure a package.json is always written.
  • Add a check after spawnSync to verify that <installRoot>/node_modules was populated, and throw an error if it's empty.
  • In doctor-bundled-plugin-runtime-deps, re-run scanBundledPluginRuntimeDeps() after repairBundledRuntimeDepsInstallRoot() returns and only emit the success note if missing.length === 0.
  • As a temporary workaround, users can manually create the bucket's package.json from .openclaw-runtime-deps.json and run npm install in-place.

Example

The suggested fix can be implemented by applying the following patch:

-    if (isolatedExecutionRoot) fs.writeFileSync(path.join(installExecutionRoot, "package.json"), `${JSON.stringify({
-      name: "openclaw-runtime-deps-install",
-      private: true
-    }, null, 2)}\n`, "utf8");
+    fs.writeFileSync(path.join(installExecutionRoot, "package.json"), `${JSON.stringify({
+      name: "openclaw-runtime-deps-install",
+      private: true
+    }, null, 2)}\n`, "utf8");

Notes

The provided patch sketch should be reviewed and tested before applying it to the production code. Additionally, the workaround provided can be used as a temporary solution until a release with the fix is shipped.

Recommendation

Apply the

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING