From 3ac4742522607b6c95a261c3f45b0f607ce9df6b Mon Sep 17 00:00:00 2001 From: rahul188 Date: Sat, 18 Jul 2026 12:57:09 +0530 Subject: [PATCH 1/3] Resolve spawned system executables by absolute path (#509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several spawn/exec calls used bare executable names and relied on PATH to resolve them at runtime, so a foreign entry prepended to PATH (shell rc files, direnv, tool shims) could cause a different binary to run. - remote-channel.ts: spawn the blocking update script with process.execPath — the exact Node binary already running the server — instead of the bare name 'node'. The path was already being logged one line above, so there is no ambiguity about which runtime to use. - setup-claude-server.js: resolve killall/pkill/open to their standard absolute locations via a small resolveSystemBinary() helper that prefers an on-disk absolute path and falls back to the bare name when the tool lives elsewhere, so distros that install it in a different directory keep working. Fixes #509 --- setup-claude-server.js | 28 +++++++++++++++++++++++----- src/remote-device/remote-channel.ts | 6 +++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/setup-claude-server.js b/setup-claude-server.js index b140ce9f..e0493c97 100755 --- a/setup-claude-server.js +++ b/setup-claude-server.js @@ -533,6 +533,19 @@ function updateSetupStep(index, status, error = null) { } } +// Resolve a system utility to an absolute path so it can't be shadowed by a +// foreign entry prepended to PATH (shell rc files, direnv, tool shims, etc.). +// Returns the first candidate that exists on disk, falling back to the bare +// name if none are found (e.g. a distro that installs the tool elsewhere). +function resolveSystemBinary(candidates, fallback) { + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate; + } + } + return fallback; +} + async function execAsync(command) { const execStep = addSetupStep(`exec_${command.substring(0, 20)}...`); return new Promise((resolve, reject) => { @@ -569,16 +582,20 @@ async function restartClaude() { `taskkill /F /IM "Claude.exe"`, ); break; - case "darwin": + case "darwin": { + const killall = resolveSystemBinary(['/usr/bin/killall'], 'killall'); await execAsync( - `killall "Claude"`, + `"${killall}" "Claude"`, ); break; - case "linux": + } + case "linux": { + const pkill = resolveSystemBinary(['/usr/bin/pkill', '/bin/pkill'], 'pkill'); await execAsync( - `pkill -f "claude"`, + `"${pkill}" -f "claude"`, ); break; + } } updateSetupStep(killStep, 'completed'); await trackEvent('npx_setup_kill_claude_success', { platform }); @@ -600,7 +617,8 @@ async function restartClaude() { updateSetupStep(startStep, 'skipped'); await trackEvent('npx_setup_start_claude_skipped', { platform }); } else if (platform === "darwin") { - await execAsync(`open -a "Claude"`); + const open = resolveSystemBinary(['/usr/bin/open'], 'open'); + await execAsync(`"${open}" -a "Claude"`); updateSetupStep(startStep, 'completed'); logToFile("\nāœ… Claude has been restarted automatically!"); await trackEvent('npx_setup_start_claude_success', { platform }); diff --git a/src/remote-device/remote-channel.ts b/src/remote-device/remote-channel.ts index 0f42478d..c5a0ac59 100644 --- a/src/remote-device/remote-channel.ts +++ b/src/remote-device/remote-channel.ts @@ -542,7 +542,11 @@ export class RemoteChannel { console.debug('[DEBUG] Spawning blocking update script:', scriptPath); console.debug('[DEBUG] Using node executable:', process.execPath); - const result = spawnSync('node', [ + // process.execPath is the exact Node binary currently running the + // server. Using it (instead of the bare name 'node') skips PATH + // resolution entirely, so the update script always runs on the same + // runtime and can't be shadowed by a different 'node' on PATH. + const result = spawnSync(process.execPath, [ scriptPath, deviceId, supabaseUrl, From 8967dcee6addfe3b97dd3d7ed882ade0f32972cc Mon Sep 17 00:00:00 2001 From: rahul188 Date: Mon, 20 Jul 2026 16:24:41 +0530 Subject: [PATCH 2/3] Resolve killall/pkill by absolute path when uninstalling too The uninstall flow still spawned these by bare name, so the PATH shadowing protection only covered setup. --- uninstall-claude-server.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/uninstall-claude-server.js b/uninstall-claude-server.js index d279602d..356cfe6b 100644 --- a/uninstall-claude-server.js +++ b/uninstall-claude-server.js @@ -412,6 +412,15 @@ function updateUninstallStep(index, status, error = null) { } } +function resolveSystemBinary(candidates, fallback) { + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate; + } + } + return fallback; +} + async function execAsync(command) { const execStep = addUninstallStep(`exec_${command.substring(0, 20)}...`); return new Promise((resolve, reject) => { @@ -489,12 +498,16 @@ async function restartClaude() { case "win32": await execAsync(`taskkill /F /IM "Claude.exe"`); break; - case "darwin": - await execAsync(`killall "Claude"`); + case "darwin": { + const killall = resolveSystemBinary(['/usr/bin/killall'], 'killall'); + await execAsync(`"${killall}" "Claude"`); break; - case "linux": - await execAsync(`pkill -f "claude"`); + } + case "linux": { + const pkill = resolveSystemBinary(['/usr/bin/pkill', '/bin/pkill'], 'pkill'); + await execAsync(`"${pkill}" -f "claude"`); break; + } } updateUninstallStep(killStep, 'completed'); logToFile("Claude process terminated successfully"); From 5359908a34e867beaf6b3b9790cc94f631ba364f Mon Sep 17 00:00:00 2001 From: rahul188 Date: Tue, 21 Jul 2026 14:41:06 +0530 Subject: [PATCH 3/3] Resolve open via absolute path in uninstall restart flow --- uninstall-claude-server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uninstall-claude-server.js b/uninstall-claude-server.js index 356cfe6b..78db0424 100644 --- a/uninstall-claude-server.js +++ b/uninstall-claude-server.js @@ -529,7 +529,8 @@ async function restartClaude() { updateUninstallStep(startStep, 'skipped'); await trackEvent('uninstall_start_claude_skipped'); } else if (platform === "darwin") { - await execAsync(`open -a "Claude"`); + const open = resolveSystemBinary(['/usr/bin/open'], 'open'); + await execAsync(`"${open}" -a "Claude"`); updateUninstallStep(startStep, 'completed'); logToFile("āœ… Claude has been restarted automatically!"); await trackEvent('uninstall_start_claude_success');