Merge pull request #89 from DenchHQ/kumareth/openclaw-install-fix

fix(bootstrap): strip npm_config_* env vars so openclaw installs to the correct global directory
This commit is contained in:
Kumar Abhirup 2026-03-10 14:36:33 -07:00 committed by GitHub
commit c912968939
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 3 deletions

View File

@ -50,7 +50,7 @@ vi.mock("./web-runtime.js", async (importOriginal) => {
type SpawnCall = {
command: string;
args: string[];
options?: { stdio?: unknown };
options?: { stdio?: unknown; env?: NodeJS.ProcessEnv };
};
function createWebProfilesResponse(params?: {
@ -1105,4 +1105,48 @@ describe("bootstrapCommand always-onboard behavior", () => {
expect(logMessages).toContain("Likely gateway cause:");
expect(logMessages).toContain("gateway.err.log");
});
it("strips npm_config_* env vars from npm global commands (prevents npx prefix hijack)", async () => {
process.env.npm_config_prefix = "/tmp/npx-fake-prefix";
process.env.npm_config_global_prefix = "/tmp/npx-fake-global";
process.env.npm_package_name = "denchclaw";
process.env.npm_lifecycle_event = "npx";
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
const npmGlobalCalls = spawnCalls.filter(
(call) =>
call.command === "npm" &&
(call.args.includes("-g") || call.args.includes("--global")),
);
expect(npmGlobalCalls.length).toBeGreaterThan(0);
for (const call of npmGlobalCalls) {
const env = call.options?.env;
expect(env).toBeDefined();
if (env) {
const leakedKeys = Object.keys(env).filter(
(key) =>
key.startsWith("npm_config_") ||
key.startsWith("npm_package_") ||
key === "npm_lifecycle_event" ||
key === "npm_lifecycle_script",
);
expect(leakedKeys).toEqual([]);
}
}
});
});

View File

@ -186,7 +186,7 @@ async function runCommandWithTimeout(
return await new Promise<SpawnResult>((resolve, reject) => {
const child = spawn(resolveCommandForPlatform(command), args, {
cwd: options.cwd,
env: options.env ? { ...process.env, ...options.env } : process.env,
env: options.env ?? process.env,
stdio,
});
let stdout = "";
@ -831,6 +831,30 @@ function createOpenClawSetupProgress(params: {
};
}
/**
* Returns a copy of `process.env` with `npm_config_*`, `npm_package_*`, and
* npm lifecycle variables stripped. When denchclaw is launched via `npx`, npm
* injects environment variables (most critically `npm_config_prefix`) that
* redirect `npm install -g` and `npm ls -g` to a temporary npx-managed
* prefix instead of the user's real global npm directory. Stripping these
* ensures child npm processes use the user's actual configuration.
*/
function cleanNpmGlobalEnv(): NodeJS.ProcessEnv {
const cleaned: NodeJS.ProcessEnv = {};
for (const [key, value] of Object.entries(process.env)) {
if (
key.startsWith("npm_config_") ||
key.startsWith("npm_package_") ||
key === "npm_lifecycle_event" ||
key === "npm_lifecycle_script"
) {
continue;
}
cleaned[key] = value;
}
return cleaned;
}
async function detectGlobalOpenClawInstall(
onOutputLine?: OutputLineHandler,
): Promise<{ installed: boolean; version?: string }> {
@ -839,6 +863,7 @@ async function detectGlobalOpenClawInstall(
{
timeoutMs: 15_000,
onOutputLine,
env: cleanNpmGlobalEnv(),
},
).catch(() => null);
@ -858,6 +883,7 @@ async function resolveNpmGlobalBinDir(
): Promise<string | undefined> {
const result = await runCommandWithTimeout(["npm", "prefix", "-g"], {
timeoutMs: 8_000,
env: cleanNpmGlobalEnv(),
onOutputLine,
}).catch(() => null);
if (!result || result.code !== 0) {
@ -946,6 +972,7 @@ async function ensureOpenClawCliAvailable(params: {
if (!globalBefore.installed) {
const install = await runCommandWithTimeout(["npm", "install", "-g", "openclaw@latest"], {
timeoutMs: 10 * 60_000,
env: cleanNpmGlobalEnv(),
onOutputLine: (line) => {
progress.output(`npm install: ${line}`);
},
@ -1021,7 +1048,9 @@ async function probeGateway(
profile: string,
gatewayPort?: number,
): Promise<{ ok: boolean; detail?: string }> {
const env = gatewayPort ? { OPENCLAW_GATEWAY_PORT: String(gatewayPort) } : undefined;
const env = gatewayPort
? { ...process.env, OPENCLAW_GATEWAY_PORT: String(gatewayPort) }
: undefined;
const result = await runOpenClaw(
openclawCommand,
["--profile", profile, "health", "--json"],