From 9c48faf1a5ddab0ecde600458eeff8be513f7de3 Mon Sep 17 00:00:00 2001 From: Leonel Rivas Date: Tue, 16 Jun 2026 07:29:46 -0700 Subject: [PATCH] fix(terminal): strip AppImage runtime env from spawned terminals On Linux AppImage builds the runtime injects APPIMAGE/APPDIR/ARGV0/OWD and prepends its temporary mount onto PATH/LD_LIBRARY_PATH. The backend inherits this from Electron and the integrated terminal inherits it from the backend, so tools inside the PTY resolve against the AppImage mount instead of the user's real environment (e.g. php reporting PHP_BINARY as the AppImage path). Scrub the AppImage runtime markers and drop only the mount segments from PATH/LD_LIBRARY_PATH in createTerminalSpawnEnv, gated on an actual AppImage launch so other environments are untouched. Fixes #1699 --- .../src/terminal/Layers/Manager.test.ts | 57 +++++++++++++++++++ apps/server/src/terminal/Layers/Manager.ts | 49 +++++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/apps/server/src/terminal/Layers/Manager.test.ts b/apps/server/src/terminal/Layers/Manager.test.ts index 8b5aa3adbc..72949905e8 100644 --- a/apps/server/src/terminal/Layers/Manager.test.ts +++ b/apps/server/src/terminal/Layers/Manager.test.ts @@ -1193,6 +1193,63 @@ it.layer( }), ); + it.effect("strips AppImage runtime env from terminal sessions", () => + Effect.gen(function* () { + const appDir = "/tmp/.mount_T3Codeabc123"; + const { manager, ptyAdapter } = yield* createManager(5, { + env: { + APPIMAGE: "/home/user/T3-Code.AppImage", + APPDIR: appDir, + ARGV0: "/home/user/T3-Code.AppImage", + OWD: "/home/user/project", + PATH: `${appDir}/usr/bin:${appDir}:/usr/local/bin:/usr/bin:/bin`, + LD_LIBRARY_PATH: `${appDir}/usr/lib:/home/user/.local/lib`, + TEST_TERMINAL_KEEP: "keep-me", + }, + }); + yield* manager.open(openInput()); + const spawnInput = ptyAdapter.spawnInputs[0]; + expect(spawnInput).toBeDefined(); + if (!spawnInput) return; + + // AppImage runtime markers must never reach the PTY — tools inside the + // terminal otherwise resolve against the AppImage mount (e.g. PHP_BINARY + // reporting the AppImage path instead of the real binary). + expect(spawnInput.env.APPIMAGE).toBeUndefined(); + expect(spawnInput.env.APPDIR).toBeUndefined(); + expect(spawnInput.env.ARGV0).toBeUndefined(); + expect(spawnInput.env.OWD).toBeUndefined(); + // PATH/LD_LIBRARY_PATH keep the user's real entries but drop the AppImage + // mount segments that the runtime prepended. + expect(spawnInput.env.PATH).toBe("/usr/local/bin:/usr/bin:/bin"); + expect(spawnInput.env.LD_LIBRARY_PATH).toBe("/home/user/.local/lib"); + // Unrelated host vars still pass through untouched. + expect(spawnInput.env.TEST_TERMINAL_KEEP).toBe("keep-me"); + }), + ); + + it.effect("leaves the environment untouched when not launched from an AppImage", () => + Effect.gen(function* () { + const { manager, ptyAdapter } = yield* createManager(5, { + env: { + PATH: "/usr/local/bin:/usr/bin:/bin", + LD_LIBRARY_PATH: "/home/user/.local/lib", + // Without APPIMAGE/APPDIR set, OWD is an ordinary variable and must + // not be stripped — only an AppImage launch gives it special meaning. + OWD: "/home/user/keep-this", + }, + }); + yield* manager.open(openInput()); + const spawnInput = ptyAdapter.spawnInputs[0]; + expect(spawnInput).toBeDefined(); + if (!spawnInput) return; + + expect(spawnInput.env.PATH).toBe("/usr/local/bin:/usr/bin:/bin"); + expect(spawnInput.env.LD_LIBRARY_PATH).toBe("/home/user/.local/lib"); + expect(spawnInput.env.OWD).toBe("/home/user/keep-this"); + }), + ); + it.effect("injects runtime env overrides into spawned terminals", () => Effect.gen(function* () { const { manager, ptyAdapter } = yield* createManager(); diff --git a/apps/server/src/terminal/Layers/Manager.ts b/apps/server/src/terminal/Layers/Manager.ts index e33d9b4b29..c3ab1c0544 100644 --- a/apps/server/src/terminal/Layers/Manager.ts +++ b/apps/server/src/terminal/Layers/Manager.ts @@ -945,6 +945,53 @@ function shouldExcludeTerminalEnvKey(key: string): boolean { return TERMINAL_ENV_BLOCKLIST.has(normalizedKey); } +// Marker variables the AppImage runtime injects into the process it launches. +// They describe the AppImage itself, not the user's session, so terminals must +// not inherit them. +const APPIMAGE_RUNTIME_ENV_KEYS = ["APPIMAGE", "APPDIR", "ARGV0", "OWD"] as const; +// PATH-style variables the AppImage runtime prepends with its temporary mount +// (e.g. /tmp/.mount_T3-XXXX/usr/bin). Only the mount segments are dropped; the +// user's real entries are preserved. +const APPIMAGE_PATH_LIKE_ENV_KEYS = ["PATH", "LD_LIBRARY_PATH"] as const; + +function isPathSegmentUnderAppDir(segment: string, appDir: string): boolean { + return segment === appDir || segment.startsWith(`${appDir}/`); +} + +// On Linux AppImage builds the runtime mounts the app under a temporary dir and +// injects APPIMAGE/APPDIR/ARGV0/OWD plus mount entries on PATH/LD_LIBRARY_PATH. +// The integrated terminal inherits the server process environment, so without +// this scrub those leak into the PTY and tools resolve against the AppImage +// mount instead of the user's real environment (e.g. `php` reporting +// PHP_BINARY as the AppImage path). See issue #1699. The scrub is gated on an +// actual AppImage launch so non-AppImage environments are left untouched. +function stripAppImageRuntimeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + if (env.APPIMAGE === undefined && env.APPDIR === undefined) return env; + + const scrubbed: NodeJS.ProcessEnv = { ...env }; + for (const key of APPIMAGE_RUNTIME_ENV_KEYS) { + delete scrubbed[key]; + } + + const appDir = env.APPDIR?.replace(/\/+$/, ""); + if (appDir) { + for (const key of APPIMAGE_PATH_LIKE_ENV_KEYS) { + const value = scrubbed[key]; + if (value === undefined) continue; + const kept = value + .split(":") + .filter((segment) => segment.length > 0 && !isPathSegmentUnderAppDir(segment, appDir)); + if (kept.length > 0) { + scrubbed[key] = kept.join(":"); + } else { + delete scrubbed[key]; + } + } + } + + return scrubbed; +} + function createTerminalSpawnEnv( baseEnv: NodeJS.ProcessEnv, runtimeEnv?: Record | null, @@ -960,7 +1007,7 @@ function createTerminalSpawnEnv( spawnEnv[key] = value; } } - return spawnEnv; + return stripAppImageRuntimeEnv(spawnEnv); } function normalizedRuntimeEnv(