authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-13 19:31:45-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-13 23:01:28-08:00
log4b4abcdd7d197647b8b21b18d2344e4e12ae4c34
tree52aa27db906ff72f2cf5dcf0f2816e0001393415
parenteb35685aeb7b891c9e0a33a1d5061c335204852b

Filter app runtime env vars from terminal spawn environment

- build terminal spawn env from a filtered copy of process env - exclude `PORT`, `T3CODE_*`, `VITE_*`, and Electron runtime vars - add test coverage to verify filtered and preserved env keys

2 files changed, 73 insertions(+), 1 deletions(-)

apps/server/src/terminalManager.test.ts+45
...@@ -424,4 +424,49 @@ describe("TerminalManager", () => {...@@ -424,4 +424,49 @@ describe("TerminalManager", () => {
424424
425 manager.dispose();425 manager.dispose();
426 });426 });
427
428 it("filters app runtime env variables from terminal sessions", async () => {
429 const originalValues = new Map<string, string | undefined>();
430 const setEnv = (key: string, value: string | undefined) => {
431 if (!originalValues.has(key)) {
432 originalValues.set(key, process.env[key]);
433 }
434 if (value === undefined) {
435 delete process.env[key];
436 return;
437 }
438 process.env[key] = value;
439 };
440 const restoreEnv = () => {
441 for (const [key, value] of originalValues) {
442 if (value === undefined) {
443 delete process.env[key];
444 } else {
445 process.env[key] = value;
446 }
447 }
448 };
449
450 setEnv("PORT", "5173");
451 setEnv("T3CODE_PORT", "3773");
452 setEnv("VITE_DEV_SERVER_URL", "http://localhost:5173");
453 setEnv("TEST_TERMINAL_KEEP", "keep-me");
454
455 try {
456 const { manager, ptyAdapter } = makeManager();
457 await manager.open(openInput());
458 const spawnInput = ptyAdapter.spawnInputs[0];
459 expect(spawnInput).toBeDefined();
460 if (!spawnInput) return;
461
462 expect(spawnInput.env.PORT).toBeUndefined();
463 expect(spawnInput.env.T3CODE_PORT).toBeUndefined();
464 expect(spawnInput.env.VITE_DEV_SERVER_URL).toBeUndefined();
465 expect(spawnInput.env.TEST_TERMINAL_KEEP).toBe("keep-me");
466
467 manager.dispose();
468 } finally {
469 restoreEnv();
470 }
471 });
427});472});
apps/server/src/terminalManager.ts+28-1
...@@ -26,6 +26,11 @@ import { runProcess } from "./processRunner";...@@ -26,6 +26,11 @@ import { runProcess } from "./processRunner";
26const DEFAULT_HISTORY_LINE_LIMIT = 5_000;26const DEFAULT_HISTORY_LINE_LIMIT = 5_000;
27const DEFAULT_PERSIST_DEBOUNCE_MS = 40;27const DEFAULT_PERSIST_DEBOUNCE_MS = 40;
28const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000;28const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000;
29const TERMINAL_ENV_BLOCKLIST = new Set([
30 "PORT",
31 "ELECTRON_RENDERER_PORT",
32 "ELECTRON_RUN_AS_NODE",
33]);
2934
30type TerminalSubprocessChecker = (terminalPid: number) => Promise<boolean>;35type TerminalSubprocessChecker = (terminalPid: number) => Promise<boolean>;
3136
...@@ -234,6 +239,27 @@ function toSessionKey(threadId: string, terminalId: string): string {...@@ -234,6 +239,27 @@ function toSessionKey(threadId: string, terminalId: string): string {
234 return `${threadId}\u0000${terminalId}`;239 return `${threadId}\u0000${terminalId}`;
235}240}
236241
242function shouldExcludeTerminalEnvKey(key: string): boolean {
243 const normalizedKey = key.toUpperCase();
244 if (normalizedKey.startsWith("T3CODE_")) {
245 return true;
246 }
247 if (normalizedKey.startsWith("VITE_")) {
248 return true;
249 }
250 return TERMINAL_ENV_BLOCKLIST.has(normalizedKey);
251}
252
253function createTerminalSpawnEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
254 const spawnEnv: NodeJS.ProcessEnv = {};
255 for (const [key, value] of Object.entries(baseEnv)) {
256 if (value === undefined) continue;
257 if (shouldExcludeTerminalEnvKey(key)) continue;
258 spawnEnv[key] = value;
259 }
260 return spawnEnv;
261}
262
237export class TerminalManager extends EventEmitter<TerminalManagerEvents> {263export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
238 private readonly sessions = new Map<string, TerminalSessionState>();264 private readonly sessions = new Map<string, TerminalSessionState>();
239 private readonly logsDir: string;265 private readonly logsDir: string;
...@@ -467,6 +493,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {...@@ -467,6 +493,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
467 let startedShell: string | null = null;493 let startedShell: string | null = null;
468 try {494 try {
469 const shellCandidates = resolveShellCandidates(this.shellResolver);495 const shellCandidates = resolveShellCandidates(this.shellResolver);
496 const terminalEnv = createTerminalSpawnEnv(process.env);
470 let lastSpawnError: unknown = null;497 let lastSpawnError: unknown = null;
471498
472 for (const shell of shellCandidates) {499 for (const shell of shellCandidates) {
...@@ -476,7 +503,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {...@@ -476,7 +503,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
476 cwd: session.cwd,503 cwd: session.cwd,
477 cols: session.cols,504 cols: session.cols,
478 rows: session.rows,505 rows: session.rows,
479 env: process.env,506 env: terminalEnv,
480 });507 });
481 startedShell = shell;508 startedShell = shell;
482 break;509 break;