openclaw - 💡(How to fix) Fix Beta blocker: Matrix runtime dependency repair still runs npm from unsafe parent directories [1 pull requests]

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…

The v2026.5.10 beta/main Matrix externalization mitigates the v2026.5.7 global-prefix deletion path for the normal configure flow, but the Matrix runtime dependency fallback itself is still unsafe: extensions/matrix/src/matrix/deps.ts computes its install cwd using fixed ../.. traversal from the compiled file and can still run npm install from an unintended parent directory.

On current main and release/2026.5.10, the fallback cwd is still:

  • old bundled/global layout: <global-prefix>/lib/node_modules
  • new external scoped package layout: <openclaw-config>/npm/node_modules/@openclaw

In the reproduced healthy managed-root case, npm walks up to OpenClaw's managed npm root, so the beta/main blast radius is contained to that managed project. That does not make the cwd safe: the command still starts from a scope directory, still prunes undeclared packages in the managed tree, and still depends on npm parent-walking behavior rather than an explicit OpenClaw-owned dependency repair API.

Error Message

ensureMatrixSdkInstalled() should become a pure availability check. If required Matrix packages are missing, it should throw a clear, actionable error that lists missing packages and tells the operator to run the central repair path, for example: Failing the guard should produce a hard error and no package-manager spawn. If npm exits 0 but required packages still do not resolve, the error should say dependency repair did not make packages resolvable, not "npm install failed." throw new Error( throw new Error(Refusing to run package manager from unsafe cwd: ${cwd}); throw new Error(Refusing to run package manager without package.json: ${cwd});

Root Cause

The v2026.5.10 beta/main Matrix externalization mitigates the v2026.5.7 global-prefix deletion path for the normal configure flow, but the Matrix runtime dependency fallback itself is still unsafe: extensions/matrix/src/matrix/deps.ts computes its install cwd using fixed ../.. traversal from the compiled file and can still run npm install from an unintended parent directory.

On current main and release/2026.5.10, the fallback cwd is still:

  • old bundled/global layout: <global-prefix>/lib/node_modules
  • new external scoped package layout: <openclaw-config>/npm/node_modules/@openclaw

In the reproduced healthy managed-root case, npm walks up to OpenClaw's managed npm root, so the beta/main blast radius is contained to that managed project. That does not make the cwd safe: the command still starts from a scope directory, still prunes undeclared packages in the managed tree, and still depends on npm parent-walking behavior rather than an explicit OpenClaw-owned dependency repair API.

Fix Action

Fix / Workaround

  1. Related upstream issue #80401 was closed as "already implemented" based on Matrix externalization. That closure is correct about the normal configure-path mitigation, but it does not address the remaining unsafe runtime fallback in deps.ts.

Externalization fixes the normal path that caused #80401 by ensuring Matrix setup goes through managed plugin installation instead of the root/global OpenClaw package. That is a good mitigation.

Code Example

openclaw dist-tags:
{
  "latest": "2026.5.7",
  "beta": "2026.5.10-beta.5"
}

@openclaw/matrix dist-tags:
{
  "latest": "2026.3.13",
  "beta": "2026.5.10-beta.5"
}

---

main                 578fad471a6a6fac9bc0c324c3c70b6e0c89c4d0
release/2026.5.10   af86c5bf6a658436d1f3453377784b92aa5e7cd2
v2026.5.10-beta.5   2a01546c81e71451caedc43e4229cc12c6129536
v2026.5.10-beta.6   3550a6c31dd330fe7fac9801cec1c7646e2e1195
v2026.5.7           3ac7453873dbc53f7892e48736c8fd28b3ea6f9c

---

function resolvePluginRoot(): string {
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
  return path.resolve(currentDir, "..", "..");
}

---

const root = resolvePluginRoot();
const command = fs.existsSync(path.join(root, "pnpm-lock.yaml"))
  ? ["pnpm", "install"]
  : ["npm", "install", "--omit=dev", "--silent"];
params.runtime.log?.(`matrix: installing dependencies via ${command[0]} (${root})...`);
const result = await runFixedCommandWithTimeout({
  argv: command,
  cwd: root,
  timeoutMs: 300_000,
  env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
});

---

node - <<'NODE'
const path = require('node:path');

for (const file of [
  '/Users/user/.openclaw/npm/node_modules/@openclaw/matrix/dist/deps-C6WqKY7m.js',
  '/opt/homebrew/lib/node_modules/openclaw/dist/deps-C6WqKY7m.js',
]) {
  const currentDir = path.dirname(file);
  console.log(`${file}
  currentDir=${currentDir}
  twoUp=${path.resolve(currentDir, '..', '..')}`);
}
NODE

---

/Users/user/.openclaw/npm/node_modules/@openclaw/matrix/dist/deps-C6WqKY7m.js
  currentDir=/Users/user/.openclaw/npm/node_modules/@openclaw/matrix/dist
  twoUp=/Users/user/.openclaw/npm/node_modules/@openclaw

/opt/homebrew/lib/node_modules/openclaw/dist/deps-C6WqKY7m.js
  currentDir=/opt/homebrew/lib/node_modules/openclaw/dist
  twoUp=/opt/homebrew/lib/node_modules

---

node <<'NODE'
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const cp = require('node:child_process');

function writeJson(file, value) {
  fs.mkdirSync(path.dirname(file), { recursive: true });
  fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
}

function sh(cmd, args, opts) {
  return cp.execFileSync(cmd, args, {
    encoding: 'utf8',
    stdio: ['ignore', 'pipe', 'pipe'],
    ...opts,
  });
}

const base = fs.mkdtempSync(path.join(os.tmpdir(), 'openclaw-managed-tar-repro-'));
const packs = path.join(base, 'packs');
fs.mkdirSync(packs);

const makePack = (name, dirName) => {
  const pkgDir = path.join(base, 'src', dirName);
  writeJson(path.join(pkgDir, 'package.json'), { name, version: '1.0.0', main: 'index.js' });
  fs.writeFileSync(path.join(pkgDir, 'index.js'), 'module.exports = {}\n');
  const out = sh('npm', ['pack', pkgDir, '--pack-destination', packs, '--silent']);
  return path.join(packs, out.trim().split('\n').at(-1));
};

const matrixTar = '/tmp/openclaw-npm-pack-inspect/openclaw-matrix-2026.5.10-beta.5.tgz';
const otherTar = makePack('@openclaw/other', 'other');
const npmRoot = path.join(base, 'managed-npm');

writeJson(path.join(npmRoot, 'package.json'), {
  private: true,
  dependencies: {
    '@openclaw/matrix': `file:${matrixTar}`,
    '@openclaw/other': `file:${otherTar}`,
  },
});

sh('npm', ['install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', '--silent'], {
  cwd: npmRoot,
});

// Simulate an undeclared package in the same managed scope.
writeJson(path.join(npmRoot, 'node_modules', '@openclaw', 'stray', 'package.json'), {
  name: '@openclaw/stray',
  version: '1.0.0',
});
fs.writeFileSync(path.join(npmRoot, 'node_modules', '@openclaw', 'stray', 'index.js'), 'module.exports = {}\n');

const cwd = path.join(npmRoot, 'node_modules', '@openclaw');

function listing() {
  return sh('find', [path.join(npmRoot, 'node_modules', '@openclaw'), '-maxdepth', '2', '-mindepth', '1', '-print'])
    .trim()
    .split('\n')
    .filter(Boolean)
    .sort()
    .map((p) => path.relative(npmRoot, p));
}

const before = listing();
let status = 0;
let stdout = '';
let stderr = '';

try {
  stdout = sh('npm', ['install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', '--silent'], { cwd });
} catch (e) {
  status = e.status ?? 1;
  stdout = e.stdout?.toString() ?? '';
  stderr = e.stderr?.toString() ?? '';
}

const after = listing();
const exactRoot = path.resolve(
  path.join(npmRoot, 'node_modules', '@openclaw', 'matrix', 'dist'),
  '..',
  '..',
);

console.log(JSON.stringify({
  npmRoot,
  cwd,
  exactRoot,
  status,
  stdout,
  stderr,
  before,
  after,
  hasRootPackageJson: fs.existsSync(path.join(npmRoot, 'package.json')),
  hasScopePackageJson: fs.existsSync(path.join(cwd, 'package.json')),
  hasRootLock: fs.existsSync(path.join(npmRoot, 'package-lock.json')),
  hasScopeLock: fs.existsSync(path.join(cwd, 'package-lock.json')),
}, null, 2));
NODE

---

{
  "cwd": ".../managed-npm/node_modules/@openclaw",
  "exactRoot": ".../managed-npm/node_modules/@openclaw",
  "status": 0,
  "before": [
    "node_modules/@openclaw/matrix",
    "node_modules/@openclaw/matrix/dist",
    "node_modules/@openclaw/matrix/openclaw.plugin.json",
    "node_modules/@openclaw/matrix/package.json",
    "node_modules/@openclaw/other",
    "node_modules/@openclaw/other/index.js",
    "node_modules/@openclaw/other/package.json",
    "node_modules/@openclaw/stray",
    "node_modules/@openclaw/stray/index.js",
    "node_modules/@openclaw/stray/package.json"
  ],
  "after": [
    "node_modules/@openclaw/matrix",
    "node_modules/@openclaw/matrix/dist",
    "node_modules/@openclaw/matrix/openclaw.plugin.json",
    "node_modules/@openclaw/matrix/package.json",
    "node_modules/@openclaw/other",
    "node_modules/@openclaw/other/index.js",
    "node_modules/@openclaw/other/package.json"
  ],
  "hasRootPackageJson": true,
  "hasScopePackageJson": false,
  "hasRootLock": true,
  "hasScopeLock": false
}

---

~/.openclaw/npm

---

<global-prefix>/lib/node_modules
<managed-root>/node_modules/@openclaw
<any>/node_modules
<any scope directory such as @openclaw>

---

path.resolve(currentDir, "..", "..")

---

npm install --omit=dev --silent

---

macOS 26.2 build 25C56
Darwin 25.2.0 arm64
Node.js v24.15.0
npm 11.12.1

---

/Users/user/.openclaw/npm/node_modules/@openclaw/matrix/dist/deps-C6WqKY7m.js
 -> /Users/user/.openclaw/npm/node_modules/@openclaw

---

/opt/homebrew/lib/node_modules/openclaw/dist/deps-C6WqKY7m.js
 -> /opt/homebrew/lib/node_modules

---

Matrix plugin dependencies are missing: matrix-js-sdk, @matrix-org/matrix-sdk-crypto-nodejs, ...
Repair this plugin with the OpenClaw plugin manager, for example:
openclaw plugins update matrix
Or run the supported diagnostic repair command where applicable:
openclaw doctor --fix

---

resolveDefaultPluginNpmDir() -> <config-dir>/npm

---

nearest package.json name must equal "@openclaw/matrix"

---

/tmp/prefix/lib/node_modules/openclaw/dist/deps.js

---

/tmp/openclaw-state/npm/node_modules/@openclaw/matrix/dist/deps.js

---

export function assertMatrixSdkAvailable(): void {
  const missing = resolveMissingMatrixPackages();
  if (missing.length === 0) {
    return;
  }
  throw new Error(
    [
      `Matrix plugin dependencies are missing: ${missing.join(", ")}`,
      `Repair this plugin with the OpenClaw plugin manager, for example: openclaw plugins update matrix`,
      `Runtime Matrix startup will not run npm install from a derived package path.`,
    ].join("\n"),
  );
}

---

function assertSafePackageInstallCwd(cwd: string, expectedKind: "managed-plugin-npm-root"): void {
  const base = path.basename(cwd);
  if (base === "node_modules" || base.startsWith("@")) {
    throw new Error(`Refusing to run package manager from unsafe cwd: ${cwd}`);
  }
  const manifestPath = path.join(cwd, "package.json");
  if (!fs.existsSync(manifestPath)) {
    throw new Error(`Refusing to run package manager without package.json: ${cwd}`);
  }
  // Also verify cwd equals resolveDefaultPluginNpmDir() or another explicit owned root.
}

---

No channel/plugin runtime may spawn npm/pnpm/yarn from a cwd inferred by parent traversal.
RAW_BUFFERClick to expand / collapse

Beta blocker: Matrix runtime dependency repair still runs npm from unsafe parent directories

Bug type

Regression (worked before, now fails)

Beta release blocker

Yes

Summary

The v2026.5.10 beta/main Matrix externalization mitigates the v2026.5.7 global-prefix deletion path for the normal configure flow, but the Matrix runtime dependency fallback itself is still unsafe: extensions/matrix/src/matrix/deps.ts computes its install cwd using fixed ../.. traversal from the compiled file and can still run npm install from an unintended parent directory.

On current main and release/2026.5.10, the fallback cwd is still:

  • old bundled/global layout: <global-prefix>/lib/node_modules
  • new external scoped package layout: <openclaw-config>/npm/node_modules/@openclaw

In the reproduced healthy managed-root case, npm walks up to OpenClaw's managed npm root, so the beta/main blast radius is contained to that managed project. That does not make the cwd safe: the command still starts from a scope directory, still prunes undeclared packages in the managed tree, and still depends on npm parent-walking behavior rather than an explicit OpenClaw-owned dependency repair API.

Current Version / Source Snapshot Checked

Checked at: 2026-05-11T18:34:28Z

Registry state at time of check:

openclaw dist-tags:
{
  "latest": "2026.5.7",
  "beta": "2026.5.10-beta.5"
}

@openclaw/matrix dist-tags:
{
  "latest": "2026.3.13",
  "beta": "2026.5.10-beta.5"
}

Git refs checked:

main                 578fad471a6a6fac9bc0c324c3c70b6e0c89c4d0
release/2026.5.10   af86c5bf6a658436d1f3453377784b92aa5e7cd2
v2026.5.10-beta.5   2a01546c81e71451caedc43e4229cc12c6129536
v2026.5.10-beta.6   3550a6c31dd330fe7fac9801cec1c7646e2e1195
v2026.5.7           3ac7453873dbc53f7892e48736c8fd28b3ea6f9c

Relevant current-main source still present:

function resolvePluginRoot(): string {
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
  return path.resolve(currentDir, "..", "..");
}

and:

const root = resolvePluginRoot();
const command = fs.existsSync(path.join(root, "pnpm-lock.yaml"))
  ? ["pnpm", "install"]
  : ["npm", "install", "--omit=dev", "--silent"];
params.runtime.log?.(`matrix: installing dependencies via ${command[0]} (${root})...`);
const result = await runFixedCommandWithTimeout({
  argv: command,
  cwd: root,
  timeoutMs: 300_000,
  env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
});

GitHub source links:

Steps to reproduce

This can be reproduced without touching a real OpenClaw install or the user's global npm prefix.

  1. Confirm current main still uses fixed two-parent traversal in extensions/matrix/src/matrix/deps.ts.

  2. Compute the cwd selected by the current fallback for both the old bundled layout and the new external package layout:

node - <<'NODE'
const path = require('node:path');

for (const file of [
  '/Users/user/.openclaw/npm/node_modules/@openclaw/matrix/dist/deps-C6WqKY7m.js',
  '/opt/homebrew/lib/node_modules/openclaw/dist/deps-C6WqKY7m.js',
]) {
  const currentDir = path.dirname(file);
  console.log(`${file}
  currentDir=${currentDir}
  twoUp=${path.resolve(currentDir, '..', '..')}`);
}
NODE

Observed output:

/Users/user/.openclaw/npm/node_modules/@openclaw/matrix/dist/deps-C6WqKY7m.js
  currentDir=/Users/user/.openclaw/npm/node_modules/@openclaw/matrix/dist
  twoUp=/Users/user/.openclaw/npm/node_modules/@openclaw

/opt/homebrew/lib/node_modules/openclaw/dist/deps-C6WqKY7m.js
  currentDir=/opt/homebrew/lib/node_modules/openclaw/dist
  twoUp=/opt/homebrew/lib/node_modules
  1. Reproduce npm behavior under an OpenClaw-like managed npm root with tarball-installed scoped packages:
node <<'NODE'
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const cp = require('node:child_process');

function writeJson(file, value) {
  fs.mkdirSync(path.dirname(file), { recursive: true });
  fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
}

function sh(cmd, args, opts) {
  return cp.execFileSync(cmd, args, {
    encoding: 'utf8',
    stdio: ['ignore', 'pipe', 'pipe'],
    ...opts,
  });
}

const base = fs.mkdtempSync(path.join(os.tmpdir(), 'openclaw-managed-tar-repro-'));
const packs = path.join(base, 'packs');
fs.mkdirSync(packs);

const makePack = (name, dirName) => {
  const pkgDir = path.join(base, 'src', dirName);
  writeJson(path.join(pkgDir, 'package.json'), { name, version: '1.0.0', main: 'index.js' });
  fs.writeFileSync(path.join(pkgDir, 'index.js'), 'module.exports = {}\n');
  const out = sh('npm', ['pack', pkgDir, '--pack-destination', packs, '--silent']);
  return path.join(packs, out.trim().split('\n').at(-1));
};

const matrixTar = '/tmp/openclaw-npm-pack-inspect/openclaw-matrix-2026.5.10-beta.5.tgz';
const otherTar = makePack('@openclaw/other', 'other');
const npmRoot = path.join(base, 'managed-npm');

writeJson(path.join(npmRoot, 'package.json'), {
  private: true,
  dependencies: {
    '@openclaw/matrix': `file:${matrixTar}`,
    '@openclaw/other': `file:${otherTar}`,
  },
});

sh('npm', ['install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', '--silent'], {
  cwd: npmRoot,
});

// Simulate an undeclared package in the same managed scope.
writeJson(path.join(npmRoot, 'node_modules', '@openclaw', 'stray', 'package.json'), {
  name: '@openclaw/stray',
  version: '1.0.0',
});
fs.writeFileSync(path.join(npmRoot, 'node_modules', '@openclaw', 'stray', 'index.js'), 'module.exports = {}\n');

const cwd = path.join(npmRoot, 'node_modules', '@openclaw');

function listing() {
  return sh('find', [path.join(npmRoot, 'node_modules', '@openclaw'), '-maxdepth', '2', '-mindepth', '1', '-print'])
    .trim()
    .split('\n')
    .filter(Boolean)
    .sort()
    .map((p) => path.relative(npmRoot, p));
}

const before = listing();
let status = 0;
let stdout = '';
let stderr = '';

try {
  stdout = sh('npm', ['install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', '--silent'], { cwd });
} catch (e) {
  status = e.status ?? 1;
  stdout = e.stdout?.toString() ?? '';
  stderr = e.stderr?.toString() ?? '';
}

const after = listing();
const exactRoot = path.resolve(
  path.join(npmRoot, 'node_modules', '@openclaw', 'matrix', 'dist'),
  '..',
  '..',
);

console.log(JSON.stringify({
  npmRoot,
  cwd,
  exactRoot,
  status,
  stdout,
  stderr,
  before,
  after,
  hasRootPackageJson: fs.existsSync(path.join(npmRoot, 'package.json')),
  hasScopePackageJson: fs.existsSync(path.join(cwd, 'package.json')),
  hasRootLock: fs.existsSync(path.join(npmRoot, 'package-lock.json')),
  hasScopeLock: fs.existsSync(path.join(cwd, 'package-lock.json')),
}, null, 2));
NODE

Observed result:

{
  "cwd": ".../managed-npm/node_modules/@openclaw",
  "exactRoot": ".../managed-npm/node_modules/@openclaw",
  "status": 0,
  "before": [
    "node_modules/@openclaw/matrix",
    "node_modules/@openclaw/matrix/dist",
    "node_modules/@openclaw/matrix/openclaw.plugin.json",
    "node_modules/@openclaw/matrix/package.json",
    "node_modules/@openclaw/other",
    "node_modules/@openclaw/other/index.js",
    "node_modules/@openclaw/other/package.json",
    "node_modules/@openclaw/stray",
    "node_modules/@openclaw/stray/index.js",
    "node_modules/@openclaw/stray/package.json"
  ],
  "after": [
    "node_modules/@openclaw/matrix",
    "node_modules/@openclaw/matrix/dist",
    "node_modules/@openclaw/matrix/openclaw.plugin.json",
    "node_modules/@openclaw/matrix/package.json",
    "node_modules/@openclaw/other",
    "node_modules/@openclaw/other/index.js",
    "node_modules/@openclaw/other/package.json"
  ],
  "hasRootPackageJson": true,
  "hasScopePackageJson": false,
  "hasRootLock": true,
  "hasScopeLock": false
}

Interpretation: in the normal managed-root case, npm walks up to the OpenClaw-managed root and reifies that project. That contains the blast radius compared with v2026.5.7, but the Matrix runtime still invoked npm from the wrong cwd and caused npm to prune an undeclared package from the managed scope. If the managed root manifest is missing/corrupt or the Matrix package is loaded from a different node_modules layout, this remains a package-manager footgun.

Expected behavior

Matrix runtime startup should never run npm install from a path derived by fixed parent traversal from import.meta.url.

Specifically:

  • runtime code should not run package-manager mutation at all, or
  • if dependency repair is retained, it should route through OpenClaw's central managed plugin install/doctor system with an explicit, validated OpenClaw-owned cwd.

Acceptable install cwd values should be explicit project roots such as:

~/.openclaw/npm

or a deliberately selected source checkout root during development, never:

<global-prefix>/lib/node_modules
<managed-root>/node_modules/@openclaw
<any>/node_modules
<any scope directory such as @openclaw>

Actual behavior

Current extensions/matrix/src/matrix/deps.ts still computes the repair cwd using:

path.resolve(currentDir, "..", "..")

That produces:

  • .../lib/node_modules for the v2026.5.7 bundled global layout
  • .../.openclaw/npm/node_modules/@openclaw for the v2026.5.10 beta external package layout

Then it runs:

npm install --omit=dev --silent

with cwd set to that computed path.

The current beta/main packaging change mitigates the specific old global-prefix deletion path during normal Matrix setup by externalizing Matrix and installing it into OpenClaw's managed plugin root. It does not fix the underlying Matrix fallback installer. The fallback remains path-shape-dependent and still performs package-manager mutation from a directory that is not a package root.

OpenClaw version

Observed in source/package inspection for:

  • openclaw@latest: 2026.5.7
  • openclaw@beta: 2026.5.10-beta.5
  • @openclaw/matrix@beta: 2026.5.10-beta.5
  • main: 578fad471a6a6fac9bc0c324c3c70b6e0c89c4d0
  • release/2026.5.10: af86c5bf6a658436d1f3453377784b92aa5e7cd2

Operating system

Evidence/repro commands run on:

macOS 26.2 build 25C56
Darwin 25.2.0 arm64
Node.js v24.15.0
npm 11.12.1

The root issue is path/cwd/package-manager behavior and is not macOS-specific.

Install method

Source inspection plus npm tarball repro. The original destructive failure was observed on npm global installs in v2026.5.7; the current beta/main residual issue is in the Matrix plugin package/runtime fallback code path.

Model

N/A - no model request involved.

Provider / routing chain

N/A - Matrix plugin dependency repair path.

Additional provider/model setup details

N/A.

Logs, screenshots, and evidence

Key evidence:

  1. main still contains the fixed-parent cwd resolver and direct npm spawn in extensions/matrix/src/matrix/deps.ts.
  2. release/2026.5.10 and v2026.5.10-beta.5/6 have the same fallback shape.
  3. Exact path resolution for the beta external package is:
/Users/user/.openclaw/npm/node_modules/@openclaw/matrix/dist/deps-C6WqKY7m.js
 -> /Users/user/.openclaw/npm/node_modules/@openclaw
  1. Exact path resolution for the old v2026.5.7 bundled global package is:
/opt/homebrew/lib/node_modules/openclaw/dist/deps-C6WqKY7m.js
 -> /opt/homebrew/lib/node_modules
  1. A temp managed-root tarball repro shows npm install from node_modules/@openclaw exits 0 and reifies the parent managed npm root, pruning an undeclared package under the same scope.

  2. Related upstream issue #80401 was closed as "already implemented" based on Matrix externalization. That closure is correct about the normal configure-path mitigation, but it does not address the remaining unsafe runtime fallback in deps.ts.

Impact and severity

Affected:

  • openclaw@latest users on 2026.5.7 remain exposed to the original global-prefix deletion path.
  • openclaw@beta / current main Matrix users are protected from that exact normal configure-flow blast radius by externalization, but the Matrix runtime fallback still computes an invalid cwd and can mutate/prune OpenClaw's managed npm tree if dependencies are missing.
  • Any unusual/corrupt plugin install layout where npm does not find the intended managed root manifest could reintroduce broader damage.

Severity:

High. Runtime code can invoke package-manager mutation from unintended parent directories. The old stable behavior can delete unrelated global CLIs. In the reproduced healthy managed-root case, the beta/main behavior is contained to OpenClaw's managed npm root, but it remains path-dependent and unsafe for critical systems.

Frequency:

The bad cwd computation is deterministic whenever ensureMatrixSdkInstalled() reaches its repair branch. It only executes when Matrix required packages are missing or resolution fails.

Consequence:

  • Stable/latest: possible deletion/pruning of unrelated global npm packages, including OpenClaw itself.
  • Beta/main: possible pruning of undeclared packages in OpenClaw's managed npm root and fragile recovery behavior if managed root metadata is damaged.
  • Operators may receive misleading "Matrix dependency install failed" errors after npm itself succeeds.

Additional information

The proper fix should be more than changing ../.. to ... The runtime should not infer package roots by parent count, and runtime channel startup should not repair npm dependencies by spawning a package manager directly.

Proposed fix

  1. Remove package-manager mutation from extensions/matrix/src/matrix/deps.ts.

ensureMatrixSdkInstalled() should become a pure availability check. If required Matrix packages are missing, it should throw a clear, actionable error that lists missing packages and tells the operator to run the central repair path, for example:

Matrix plugin dependencies are missing: matrix-js-sdk, @matrix-org/matrix-sdk-crypto-nodejs, ...
Repair this plugin with the OpenClaw plugin manager, for example:
openclaw plugins update matrix
Or run the supported diagnostic repair command where applicable:
openclaw doctor --fix

Exact command text should match the current supported repair command.

  1. Route all dependency installation/repair through the existing managed plugin install system.

Matrix already declares runtime dependencies in @openclaw/matrix/package.json. The only code that should run npm for this should be the plugin install/update/doctor code, using the managed npm root:

resolveDefaultPluginNpmDir() -> <config-dir>/npm

That path has an explicit package.json and uses the central safe npm env/args.

  1. Add a hard cwd safety guard for every npm/pnpm install invocation.

Before spawning a package manager, assert:

  • cwd exists
  • cwd contains package.json
  • cwd is an expected OpenClaw-owned project root
  • cwd basename is not node_modules
  • cwd basename does not start with @
  • cwd is not under a global npm prefix package directory such as /opt/homebrew/lib/node_modules, /usr/local/lib/node_modules, or ~/.npm-global/lib/node_modules
  • cwd is not inside a package's dist/ ancestry unless deliberately running in a source checkout

Failing the guard should produce a hard error and no package-manager spawn.

  1. If a package root resolver remains necessary, replace fixed parent traversal with package-manifest discovery.

A safe helper should walk upward from import.meta.url to the nearest package.json, then verify the package identity. For Matrix:

nearest package.json name must equal "@openclaw/matrix"

But even if this finds the package root, runtime code should still not run npm install inside an installed package under node_modules. It should only use that root for diagnostics.

  1. Add regression coverage.

Minimum test cases:

  • Bundled v2026.5.7-style path:
/tmp/prefix/lib/node_modules/openclaw/dist/deps.js

Missing deps must not trigger npm with cwd /tmp/prefix/lib/node_modules.

  • External beta-style path:
/tmp/openclaw-state/npm/node_modules/@openclaw/matrix/dist/deps.js

Missing deps must not trigger npm with cwd /tmp/openclaw-state/npm/node_modules/@openclaw.

  • Healthy managed root:

Dependency repair must run only through the central plugin install/doctor path with cwd /tmp/openclaw-state/npm.

  • Corrupt managed root:

If /tmp/openclaw-state/npm/package.json is absent/invalid, repair must fail closed before running npm.

  • Global-prefix preservation:

A fake global prefix containing openclaw and another CLI package must remain unchanged after any Matrix missing-dependency flow.

  • No misleading success/failure:

If npm exits 0 but required packages still do not resolve, the error should say dependency repair did not make packages resolvable, not "npm install failed."

Suggested implementation sketch

In Matrix runtime:

export function assertMatrixSdkAvailable(): void {
  const missing = resolveMissingMatrixPackages();
  if (missing.length === 0) {
    return;
  }
  throw new Error(
    [
      `Matrix plugin dependencies are missing: ${missing.join(", ")}`,
      `Repair this plugin with the OpenClaw plugin manager, for example: openclaw plugins update matrix`,
      `Runtime Matrix startup will not run npm install from a derived package path.`,
    ].join("\n"),
  );
}

In core plugin repair/install:

function assertSafePackageInstallCwd(cwd: string, expectedKind: "managed-plugin-npm-root"): void {
  const base = path.basename(cwd);
  if (base === "node_modules" || base.startsWith("@")) {
    throw new Error(`Refusing to run package manager from unsafe cwd: ${cwd}`);
  }
  const manifestPath = path.join(cwd, "package.json");
  if (!fs.existsSync(manifestPath)) {
    throw new Error(`Refusing to run package manager without package.json: ${cwd}`);
  }
  // Also verify cwd equals resolveDefaultPluginNpmDir() or another explicit owned root.
}

Why externalization alone is not sufficient

Externalization fixes the normal path that caused #80401 by ensuring Matrix setup goes through managed plugin installation instead of the root/global OpenClaw package. That is a good mitigation.

However, deps.ts still contains an independent runtime repair branch. Any future missing-dependency state, package integrity issue, partial install, operator cleanup, lockfile drift, or plugin-manager bug can reach that branch. When it does, the branch still derives cwd from compiled-file layout instead of from an explicit managed install root.

For critical systems, the invariant should be:

No channel/plugin runtime may spawn npm/pnpm/yarn from a cwd inferred by parent traversal.

Linked Issue/PR

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

Matrix runtime startup should never run npm install from a path derived by fixed parent traversal from import.meta.url.

Specifically:

  • runtime code should not run package-manager mutation at all, or
  • if dependency repair is retained, it should route through OpenClaw's central managed plugin install/doctor system with an explicit, validated OpenClaw-owned cwd.

Acceptable install cwd values should be explicit project roots such as:

~/.openclaw/npm

or a deliberately selected source checkout root during development, never:

<global-prefix>/lib/node_modules
<managed-root>/node_modules/@openclaw
<any>/node_modules
<any scope directory such as @openclaw>

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 Beta blocker: Matrix runtime dependency repair still runs npm from unsafe parent directories [1 pull requests]