From d381c486abbb4b2dfbdc7d65cfb00de769a0644e Mon Sep 17 00:00:00 2001 From: hzhang Date: Sun, 31 May 2026 21:04:53 +0100 Subject: [PATCH] fix(bridge): strip NODE_OPTIONS --inspect before spawning claude/gemini MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit claude-code and gemini-cli are both Node binaries. When the parent gateway is launched with `NODE_OPTIONS=--inspect=127.0.0.1:9229` (for debugging), spawn(child).env = {...process.env} propagates the flag into the child. The child Node then tries to bind the same inspector port, fails EADDRINUSE, and exits SILENTLY (no stdout, no stderr). Bridge sees an empty stream and reports `claude did not return a session_id` with an empty stderr summary — extremely opaque diagnostic that took non-trivial digging to root-cause. Sanitize NODE_OPTIONS before spawn: keep everything except `--inspect*` / `--inspect-brk*` / `--debug*`. Operators that legitimately need other NODE_OPTIONS values (e.g. `--max-old-space-size`) keep them. Verified end-user repro on prod-t2 2026-05-31: with `Environment=NODE_OPTIONS=--inspect=127.0.0.1:9229` in the gateway systemd drop-in, `claude -p "hi" --output-format stream-json --verbose` spawned from the bridge returned ZERO bytes; running the exact same command from a shell without the env var returned the full init → assistant → result stream in ~6s. Surfaced recruiting developer1 (Cody, contractor-claude-bridge). --- plugin/core/claude/sdk-adapter.ts | 21 ++++++++++++++++++++- plugin/core/gemini/sdk-adapter.ts | 16 +++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/plugin/core/claude/sdk-adapter.ts b/plugin/core/claude/sdk-adapter.ts index 96d1349..7467320 100644 --- a/plugin/core/claude/sdk-adapter.ts +++ b/plugin/core/claude/sdk-adapter.ts @@ -158,10 +158,29 @@ export async function* dispatchToClaude( // detached:true puts claude in its own process group. Claude's Bash tool // occasionally leaks shells/ssh that keep claude alive past end-of-turn; when // that happens we SIGKILL the whole group rather than wait forever. + // Sanitize NODE_OPTIONS before spawning. Claude Code is a Node CLI; if + // the parent gateway runs with `NODE_OPTIONS=--inspect=...:9229`, every + // child Node process — including claude — tries to bind the same inspector + // port, fails (EADDRINUSE), and exits SILENTLY (no stdout, no stderr). + // Bridge then sees an empty stream and reports `claude did not return a + // session_id` with no useful diagnostic. Strip any --inspect* / + // --inspect-brk* / --debug* flag from NODE_OPTIONS; keep everything else + // (e.g. --max-old-space-size) in case operators depend on it. + const childEnv: NodeJS.ProcessEnv = { ...process.env }; + if (childEnv.NODE_OPTIONS) { + const filtered = childEnv.NODE_OPTIONS + .split(/\s+/) + .filter((tok) => tok && !tok.startsWith("--inspect") && !tok.startsWith("--debug")) + .join(" ") + .trim(); + if (filtered) childEnv.NODE_OPTIONS = filtered; + else delete childEnv.NODE_OPTIONS; + } + const child = spawn("claude", args, { cwd: workspace, stdio: ["ignore", "pipe", "pipe"], - env: { ...process.env }, + env: childEnv, detached: true, }); diff --git a/plugin/core/gemini/sdk-adapter.ts b/plugin/core/gemini/sdk-adapter.ts index d3d935e..ab71436 100644 --- a/plugin/core/gemini/sdk-adapter.ts +++ b/plugin/core/gemini/sdk-adapter.ts @@ -156,10 +156,24 @@ export async function* dispatchToGemini( args.push("--resume", resumeSessionId); } + // Sanitize NODE_OPTIONS before spawning — same reason as the claude + // adapter: gemini-cli is a Node binary; inheriting a parent + // `NODE_OPTIONS=--inspect=...:9229` makes every child silently EADDRINUSE. + const childEnv: NodeJS.ProcessEnv = { ...process.env }; + if (childEnv.NODE_OPTIONS) { + const filtered = childEnv.NODE_OPTIONS + .split(/\s+/) + .filter((tok) => tok && !tok.startsWith("--inspect") && !tok.startsWith("--debug")) + .join(" ") + .trim(); + if (filtered) childEnv.NODE_OPTIONS = filtered; + else delete childEnv.NODE_OPTIONS; + } + const child = spawn("gemini", args, { cwd: workspace, stdio: ["ignore", "pipe", "pipe"], - env: { ...process.env }, + env: childEnv, }); const stderrLines: string[] = [];