Files
Bohdan Triapitsyn ff75dc9bd5 fix(terminal): unset NODE_CHANNEL_FD for PTY shells on every POSIX host
The PTY runtime exported an empty NODE_CHANNEL_FD to override the daemon's
IPC descriptor, because bun-pty merges the native environ back into the
child and a JS-only delete does not stick. Node CLIs launched from the
shell (opencode, claude) then printed "warn: Failed to parse IPC channel
number ''" on exit.

The Linux-only env -u ARGV0 wrapper now applies on macOS and Linux and
also unsets NODE_CHANNEL_FD, so the variable is gone instead of empty.

Testing: runtime and inherited-env tests updated for the POSIX wrapper;
verified in the running app that exiting opencode no longer prints the
warning.
2026-09-07 12:18:22 +03:00

82 lines
3.0 KiB
JavaScript

/**
* Sanitize environment objects inherited by user-facing child processes.
*
* Linux AppImage runtimes export `ARGV0` as the AppImage path before launching
* the packaged app. zsh treats an exported `ARGV0` as the argv[0] for every
* external command it spawns, which corrupts Python venv detection and any
* other program that reads argv[0]/$0 while leaving `/proc/self/exe` correct.
*
* See openchamber/openchamber#2588 and pingdotgg/t3code#2509.
*/
import { createRequire } from 'node:module';
import { existsSync } from 'node:fs';
const POSIX_ENV_BINARIES = ['/usr/bin/env', '/bin/env'];
/** Variables a PTY shell must never inherit from the OpenChamber host process. */
const PTY_HOST_PRIVATE_VARIABLES = Object.freeze(['ARGV0', 'NODE_CHANNEL_FD']);
/**
* Remove AppImage `ARGV0` from a mutable env object (or `process.env`).
* @param {NodeJS.ProcessEnv | Record<string, string | undefined> | null | undefined} env
* @returns {typeof env}
*/
export function stripAppImageArgv0Leak(env) {
if (!env || typeof env !== 'object') return env;
if (Object.prototype.hasOwnProperty.call(env, 'ARGV0')) {
delete env.ARGV0;
}
return env;
}
/**
* Clear AppImage `ARGV0` from this process.
*
* Bun keeps a native environ that `bun-pty` inherits even after
* `delete process.env.ARGV0`. On Linux under Bun we also call libc `unsetenv`.
*/
export function clearAppImageArgv0FromProcessEnv() {
delete process.env.ARGV0;
if (process.platform !== 'linux' || typeof Bun === 'undefined') return;
try {
const require = createRequire(import.meta.url);
const { dlopen } = require('bun:ffi');
const libc = dlopen('libc.so.6', {
unsetenv: { args: ['cstring'], returns: 'i32' },
});
libc.symbols.unsetenv(Buffer.from('ARGV0\0'));
} catch {
// Node/Electron and environments without bun:ffi rely on explicit child envs.
}
}
/**
* Resolve a POSIX PTY launch that unsets host-private variables before the
* shell starts.
*
* `bun-pty` merges the OS environ into the child, so deleting a variable from
* the JS env object alone is not enough: AppImage `ARGV0` came back on Linux,
* and the daemon's `NODE_CHANNEL_FD` came back everywhere (Node then warns
* "Failed to parse IPC channel number" when a CLI such as opencode exits).
* Wrapping with `env -u NAME ...` unsets them before execing the real shell.
* No-op on Windows and when no `env` binary is found.
*
* @param {string} executable
* @param {string[]} args
* @param {readonly string[]} variables
* @returns {{ executable: string, args: string[] }}
*/
export function resolvePosixPtyLaunch(executable, args = [], variables = PTY_HOST_PRIVATE_VARIABLES) {
if (process.platform === 'win32' || variables.length === 0) {
return { executable, args };
}
const envBinary = POSIX_ENV_BINARIES.find((candidate) => existsSync(candidate));
if (!envBinary) {
return { executable, args };
}
return {
executable: envBinary,
args: [...variables.flatMap((name) => ['-u', name]), executable, ...args],
};
}