fix(update): support npm before lifecycle allowlists (#125452)

This commit is contained in:
Peter Steinberger
2026-08-17 17:01:23 -07:00
committed by GitHub
parent b228c83bfc
commit a996ea25d9
18 changed files with 56 additions and 144 deletions
+4 -4
View File
@@ -39,10 +39,10 @@ Already manage Node.js? Install the published package instead (Node 22.22.3+, 24
npm install -g openclaw@latest --allow-scripts=openclaw
```
That command is for npm 12 or npm 11.16+. On npm 11.12 and earlier, omit
`--allow-scripts=openclaw`. If you have npm 11.1311.15, upgrade npm first. See
the [installation guide](https://docs.openclaw.ai/install) for the lifecycle
script contract, Docker, Nix, and other deployment paths.
That command is for npm 12 or npm 11.16+. On npm 11.15 and earlier, omit
`--allow-scripts=openclaw`. See the
[installation guide](https://docs.openclaw.ai/install) for the lifecycle script
contract, Docker, Nix, and other deployment paths.
## Quick start
+1 -2
View File
@@ -148,8 +148,7 @@ The Gateway is the single source of truth for sessions, routing, and channel con
npm install -g openclaw@latest --allow-scripts=openclaw
```
On npm 11.12 and earlier, omit `--allow-scripts=openclaw`. Upgrade npm
11.1311.15 before installing.
On npm 11.15 and earlier, omit `--allow-scripts=openclaw`.
</Step>
<Step title="Onboard and install the service">
+2 -2
View File
@@ -242,8 +242,8 @@ personal-number mode, and self-chat details: [WhatsApp](/channels/whatsapp).
The snapshot's global npm tree is owned by root, so plain `openclaw update`
cannot write to it. Update from the sandbox SSH session with:
The command below is for npm 12 or npm 11.16+. On npm 11.12 and earlier,
omit `--allow-scripts=openclaw`; upgrade npm 11.1311.15 first.
The command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier,
omit `--allow-scripts=openclaw`.
```bash
sudo env "PATH=$PATH" npm install --global openclaw@latest --allow-scripts=openclaw
+4 -7
View File
@@ -82,9 +82,8 @@ If you already manage Node yourself:
openclaw onboard --install-daemon
```
On npm 11.12 and earlier, use the same command without
`--allow-scripts=openclaw`. Do not use npm 11.1311.15 for this install;
upgrade to npm 11.16+ first.
On npm 11.15 and earlier, use the same command without
`--allow-scripts=openclaw`.
<Note>
npm 12 blocks unapproved package lifecycle scripts by default. The
@@ -93,11 +92,9 @@ If you already manage Node yourself:
they are not covered by allowScripts`.
npm 11.16 accepts the option but otherwise only warns that the scripts are
`not yet covered by allowScripts` and still runs them. npm 11.12 and earlier
`not yet covered by allowScripts` and still runs them. npm 11.15 and earlier
have neither the policy nor the option, so their command must be unflagged.
npm 11.1311.15 also lack the option, but they are transitional upstream
releases outside this documented install contract; upgrade rather than
relying on their unflagged behavior. The `npm approve-scripts openclaw`
The `npm approve-scripts openclaw`
command suggested by npm 11.16 does not work for a global install — it fails
with `ENOMATCH No installed packages match: openclaw`.
</Note>
+2 -2
View File
@@ -116,8 +116,8 @@ Replace `youruser` with the account you created, and the IP with your VM's IP.
## 6) Install OpenClaw
Inside the VM, use the following command on npm 12 or npm 11.16+. On npm 11.12
and earlier, omit `--allow-scripts=openclaw`; upgrade npm 11.1311.15 first.
Inside the VM, use the following command on npm 12 or npm 11.16+. On npm 11.15
and earlier, omit `--allow-scripts=openclaw`.
```bash
npm install -g openclaw@latest --allow-scripts=openclaw
+11 -11
View File
@@ -49,8 +49,8 @@ successful core update; a direct
`npm install -g openclaw@extended-stable --allow-scripts=openclaw` does not
update `update.channel`, but a final extended-stable package version still
checks only the verified `extended-stable` selector for update availability.
That direct command is for npm 12 or npm 11.16+. On npm 11.12 and earlier,
omit `--allow-scripts=openclaw`; upgrade npm 11.1311.15 first.
That direct command is for npm 12 or npm 11.16+. On npm 11.15 and earlier,
omit `--allow-scripts=openclaw`.
After the core swap, eligible official npm plugins with bare/default or
`latest` intent converge to that exact core version. Exact pins and explicit
non-`latest` tags, third-party plugins, and non-npm sources remain unchanged.
@@ -157,8 +157,8 @@ curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method npm --ve
## Alternative: manual npm, pnpm, or bun
The npm command below is for npm 12 or npm 11.16+. On npm 11.12 and earlier,
omit `--allow-scripts=openclaw`; upgrade npm 11.1311.15 first.
The npm command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier,
omit `--allow-scripts=openclaw`.
```bash
npm i -g openclaw@latest --allow-scripts=openclaw
@@ -178,7 +178,7 @@ that Gateway. Replace `/usr/bin/npm` with the system npm that owns the
root-owned global prefix on your host:
The npm command below follows the same version contract: use the flag on npm 12
or npm 11.16+, omit it on npm 11.12 and earlier, and upgrade npm 11.1311.15.
or npm 11.16+, and omit it on npm 11.15 and earlier.
```bash
openclaw gateway stop
@@ -203,10 +203,10 @@ Node version during `preinstall`; only then does OpenClaw verify the packaged
`dist` inventory and swap the clean package tree into the real global prefix. A
packed completion guard is omitted from the expected inventory and removed only
after `preinstall` succeeds, so skipped lifecycle scripts also fail before the
swap. The updater probes the owning npm before mutation. On npm 11.12 and
earlier it omits the unsupported lifecycle-policy flag; on npm 11.1311.15 it
stops with upgrade guidance. On npm 12 and npm 11.16+, it approves only the
candidate OpenClaw lifecycle; transitive dependency scripts remain unapproved.
swap. The updater probes the owning npm before mutation. On npm 11.15 and
earlier it omits the unsupported lifecycle-policy flag. On npm 12 and npm
11.16+, it approves only the candidate OpenClaw lifecycle; transitive
dependency scripts remain unapproved.
This avoids npm overlaying a new package onto stale files from the old one. If
the install command fails, OpenClaw retries once with `--omit=optional`, which
helps hosts where native optional dependencies cannot compile.
@@ -445,8 +445,8 @@ automatically replacing the package again.
If the CLI update path is unavailable, use the same package manager and install
scope that own the current Gateway:
The npm command below is for npm 12 or npm 11.16+. On npm 11.12 and earlier,
omit `--allow-scripts=openclaw`; upgrade npm 11.1311.15 first.
The npm command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier,
omit `--allow-scripts=openclaw`.
```bash
openclaw gateway stop
+2 -2
View File
@@ -39,8 +39,8 @@ The keepalive options reduce idle tunnel drops during onboarding.
## Install OpenClaw
Inside the Box, use the following command on npm 12 or npm 11.16+. On npm 11.12
and earlier, omit `--allow-scripts=openclaw`; upgrade npm 11.1311.15 first.
Inside the Box, use the following command on npm 12 or npm 11.16+. On npm 11.15
and earlier, omit `--allow-scripts=openclaw`.
```bash
sudo npm install -g openclaw --allow-scripts=openclaw
+1 -1
View File
@@ -88,7 +88,7 @@ RPCs: `environments.create`, `environments.destroy`, extended `environments.list
No bespoke worker artifact, and no dependence on npm availability:
- Canonical install for all modes: a gateway-produced, content-hashed worker bundle (the gateway's own build output packed as a tarball), pushed over SSH and installed on the box. This covers dev builds and unreleased commits by construction.
- On npm 12 or npm 11.16+, `npm i -g openclaw@<exact gateway version> --allow-scripts=openclaw` is an optimization when the gateway runs a released version; never `latest`. On npm 11.12 and earlier, omit `--allow-scripts=openclaw`; upgrade npm 11.1311.15 first.
- On npm 12 or npm 11.16+, `npm i -g openclaw@<exact gateway version> --allow-scripts=openclaw` is an optimization when the gateway runs a released version; never `latest`. On npm 11.15 and earlier, omit `--allow-scripts=openclaw`.
- Bootstrap is idempotent; a warm lease with a matching bundle hash skips install. Raw machines may need a networked toolchain phase (Node runtime) — part of the setup phase, closed afterwards.
- Handshake verifies worker build hash, protocol feature set, and runtime compatibility. The existing gateway version/protocol checks are insufficient for this (SSH-tunneled nodes are exempted from exact-version rejection), so worker admission does its own exact-build check.
+2 -2
View File
@@ -54,8 +54,8 @@ Full server guidance lives in the [Linux guide](/platforms/linux) and the
On a single user Chromebook, use the native npm install (the installer script,
or `npm i -g openclaw@latest --allow-scripts=openclaw` on npm 12 or npm
11.16+) rather than [Docker](/install/docker). On npm 11.12 and earlier, omit
`--allow-scripts=openclaw`; upgrade npm 11.1311.15 first.
11.16+) rather than [Docker](/install/docker). On npm 11.15 and earlier, omit
`--allow-scripts=openclaw`.
Docker works inside Crostini, but Docker in Crostini adds friction: if you use
the Claude Code CLI as your model runtime, it has to be installed and logged in
+1 -1
View File
@@ -130,7 +130,7 @@ Linux v1 uses one Canvas window. HTTP and HTTPS pages are renderable, but A2UI a
The CLI remains the simplest option for a headless server, a VPS, or a remote Gateway:
1. Install Node 26 (recommended), or another supported release: Node 22.22.3+, Node 24.15+, or Node 25.9+.
2. On npm 12 or npm 11.16+, run `npm i -g openclaw@latest --allow-scripts=openclaw`. On npm 11.12 and earlier, omit `--allow-scripts=openclaw`; upgrade npm 11.1311.15 first.
2. On npm 12 or npm 11.16+, run `npm i -g openclaw@latest --allow-scripts=openclaw`. On npm 11.15 and earlier, omit `--allow-scripts=openclaw`.
3. `openclaw onboard --install-daemon`
4. From your laptop: `ssh -N -L 18789:127.0.0.1:18789 <user>@<host>`
5. Open `http://127.0.0.1:18789/` and authenticate with the configured shared
+2 -2
View File
@@ -29,8 +29,8 @@ OpenClaw package.
For a manual install, use Node 26 (recommended) or another supported release:
Node 22.22.3+, Node 24.15+, or Node 25.9+. Install `openclaw` globally:
The command below is for npm 12 or npm 11.16+. On npm 11.12 and earlier,
omit `--allow-scripts=openclaw`; upgrade npm 11.1311.15 first.
The command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier,
omit `--allow-scripts=openclaw`.
```bash
npm install -g openclaw@<version> --allow-scripts=openclaw
+2 -2
View File
@@ -55,8 +55,8 @@ matching user-space CLI and runtime before starting the Gateway wizard.
For manual development recovery, install the matching CLI yourself:
The npm command below is for npm 12 or npm 11.16+. On npm 11.12 and earlier,
omit `--allow-scripts=openclaw`; upgrade npm 11.1311.15 first.
The npm command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier,
omit `--allow-scripts=openclaw`.
```bash
npm install -g openclaw@<version> --allow-scripts=openclaw
+3 -3
View File
@@ -207,9 +207,9 @@ the repository root does not prepare bundled plugin dependencies.
For the global npm row, use
`npm install -g openclaw --allow-scripts=openclaw` on npm 12 or npm 11.16+.
On npm 11.12 and earlier, omit `--allow-scripts=openclaw`; upgrade npm
11.1311.15 first. Plugin dependency convergence remains intentionally
script-disabled and continues to use the `--ignore-scripts` commands above.
On npm 11.15 and earlier, omit `--allow-scripts=openclaw`. Plugin dependency
convergence remains intentionally script-disabled and continues to use the
`--ignore-scripts` commands above.
## Legacy cleanup
-54
View File
@@ -4717,34 +4717,6 @@ describe("update-cli", () => {
expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1);
});
it.each(["11.13.0", "11.15.9"])(
"refuses npm %s before stopping the managed gateway or cleaning update backups",
async (npmVersion) => {
const tempDir = await createTrackedTempDir("openclaw-update-npm-policy-");
const { nodeModules, entryPath } = await setupInstalledPackageRoot(tempDir);
const backupDir = path.join(nodeModules, ".openclaw-interrupted");
await fs.mkdir(backupDir, { recursive: true });
mockRunningManagedGateway(["node", entryPath, "gateway", "run"]);
mockFileBackedPathExists();
vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => {
if (argv[0] === "npm" && argv[1] === "--version") {
return commandResult({ stdout: `${npmVersion}\n` });
}
if (argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") {
return commandResult({ stdout: `${nodeModules}\n` });
}
return commandResult();
});
await updateCommand({ yes: true });
expect(serviceStop).not.toHaveBeenCalled();
expect(packageInstallCommandCall()).toBeUndefined();
await expect(fs.access(backupDir)).resolves.toBeUndefined();
expect(getErrorOutput()).toContain(`npm ${npmVersion} cannot safely approve`);
},
);
it("stops a running managed gateway before package replacement", async () => {
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const processOnSpy = vi.spyOn(process, "on");
@@ -6929,32 +6901,6 @@ describe("update-cli", () => {
expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1);
});
it.each(["11.13.0", "11.15.9"])(
"refuses npm %s package-to-dev updates before checkout or install",
async (npmVersion) => {
const packageRoot = createCaseDir("openclaw-npm-transition");
mockPackageInstallStatus(packageRoot);
vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => {
if (argv[0] === "npm" && argv[1] === "--version") {
return commandResult({ stdout: `${npmVersion}\n` });
}
if (argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") {
return commandResult({ stdout: `${path.dirname(packageRoot)}\n` });
}
return commandResult();
});
await updateCommand({ channel: "dev", yes: true, restart: false });
expect(runGatewayUpdate).not.toHaveBeenCalled();
expect(commandCalls().some(([argv]) => argv[0] === "git")).toBe(false);
expect(
commandCalls().some(([argv]) => argv[0] === "npm" && argv[1] === "i" && argv[2] === "-g"),
).toBe(false);
expect(getErrorOutput()).toContain(`npm ${npmVersion} cannot safely approve`);
},
);
it("explains why git updates cannot run with edited files", async () => {
vi.mocked(defaultRuntime.log).mockClear();
vi.mocked(defaultRuntime.error).mockClear();
+5 -9
View File
@@ -163,18 +163,14 @@ describe("markPackagePostInstallDoctorAdvisory", () => {
});
describe("npm lifecycle policy preflight", () => {
it.each([
{ version: "11.13.0", message: "Upgrade the owning npm to 11.16" },
{ version: "11.15.9", message: "Upgrade the owning npm to 11.16" },
{ version: null, message: "Unable to determine the owning npm version" },
])("stops before mutation for npm $version", async ({ version, message }) => {
it("stops before mutation when the owning npm version is unknown", async () => {
const runStep = vi.fn();
const runCommand = vi.fn<CommandRunner>();
const installTarget = createNpmTarget("/tmp/npm-policy-test/lib/node_modules");
installTarget.npmOwner = {
version,
lifecyclePolicy: version ? "unsupported-transition" : null,
...(version ? {} : { probeError: "version probe failed" }),
version: null,
lifecyclePolicy: null,
probeError: "version probe failed",
};
const result = await runGlobalPackageUpdateSteps({
@@ -187,7 +183,7 @@ describe("npm lifecycle policy preflight", () => {
});
expect(runCommand).not.toHaveBeenCalled();
expect(result.failedStep?.stderrTail).toContain(message);
expect(result.failedStep?.stderrTail).toContain("Unable to determine the owning npm version");
expect(runStep).not.toHaveBeenCalled();
});
});
+9 -3
View File
@@ -213,9 +213,9 @@ describe("update global helpers", () => {
it.each([
["11.12.0", "unflagged"],
["11.13.0", "unsupported-transition"],
["11.14.0", "unsupported-transition"],
["11.15.9", "unsupported-transition"],
["11.13.0", "unflagged"],
["11.14.0", "unflagged"],
["11.15.9", "unflagged"],
["11.16.0", "allow-scripts"],
["12.0.0", "allow-scripts"],
] as const)("binds npm %s lifecycle policy to the owning executable", async (version, policy) => {
@@ -775,6 +775,12 @@ describe("update global helpers", () => {
]);
});
it("omits npm's lifecycle allowlist before npm 11.16", () => {
expect(
globalInstallArgs("npm", "openclaw@latest", null, null, null, "unflagged"),
).not.toContain("--allow-scripts=openclaw");
});
it("allows only the resolved npm candidate lifecycle identity", () => {
expect(globalInstallArgs("npm", "/tmp/openclaw-2026.7.2.tgz")).toContain(
"--allow-scripts=/tmp/openclaw-2026.7.2.tgz",
+5 -16
View File
@@ -83,9 +83,9 @@ export type NpmGlobalPrefixLayout = {
binDir: string;
};
type NpmLifecyclePolicy = "unsupported-transition" | "unflagged" | "allow-scripts";
type NpmLifecyclePolicy = "unflagged" | "allow-scripts";
type SupportedNpmLifecyclePolicy = Exclude<NpmLifecyclePolicy, "unsupported-transition">;
type SupportedNpmLifecyclePolicy = NpmLifecyclePolicy;
type NpmLifecyclePolicyGate =
| { policy: SupportedNpmLifecyclePolicy | null; error: null }
@@ -97,13 +97,9 @@ function resolveNpmLifecyclePolicy(version: string): NpmLifecyclePolicy | null {
if (!parsed) {
return null;
}
if (parsed.major !== 11) {
return parsed.major >= 12 ? "allow-scripts" : "unflagged";
}
if (parsed.minor <= 12) {
return "unflagged";
}
return parsed.minor >= 16 ? "allow-scripts" : "unsupported-transition";
return parsed.major >= 12 || (parsed.major === 11 && parsed.minor >= 16)
? "allow-scripts"
: "unflagged";
}
/** Resolves the owning npm policy once, before any update mutation. */
@@ -113,17 +109,10 @@ export function resolveNpmLifecyclePolicyGate(
if (installTarget.manager !== "npm") {
return { policy: null, error: null };
}
const version = installTarget.npmOwner?.version ?? "";
const policy = installTarget.npmOwner?.lifecyclePolicy ?? null;
if (policy === "unflagged" || policy === "allow-scripts") {
return { policy, error: null };
}
if (policy === "unsupported-transition") {
return {
policy: null,
error: `npm ${version} cannot safely approve OpenClaw lifecycle scripts. Upgrade the owning npm to 11.16 or newer before updating; no package changes were made.`,
};
}
return {
policy: null,
error: `Unable to determine the owning npm version before updating; no package changes were made.${installTarget.npmOwner?.probeError ? ` ${installTarget.npmOwner.probeError}` : ""}`,
-21
View File
@@ -3013,27 +3013,6 @@ describe("runGatewayUpdate", () => {
expect(await pathExists(staleDir)).toBe(false);
});
it("refuses unsupported npm before global update cleanup mutates backups", async () => {
const { nodeModules, pkgRoot } = await createGlobalPackageFixture(tempDir);
const backupDir = path.join(nodeModules, ".openclaw-interrupted");
await fs.mkdir(backupDir, { recursive: true });
const { runCommand } = createGlobalInstallHarness({
pkgRoot,
npmRootOutput: nodeModules,
npmVersion: "11.15.9",
installCommand: npmGlobalInstallCommand("openclaw@latest"),
});
const result = await runWithCommand(runCommand, { cwd: pkgRoot });
expect(result).toMatchObject({
status: "error",
reason: "unexpected-error",
});
expect(result.steps.at(-1)?.name).toBe("npm lifecycle policy preflight");
await expect(fs.access(backupDir)).resolves.toBeUndefined();
});
it("retries global npm update with --omit=optional when initial install fails", async () => {
const nodeModules = path.join(tempDir, "node_modules");
const pkgRoot = path.join(nodeModules, "openclaw");