From 30bf7408282371f02356a5416aa0935d7c1fe224 Mon Sep 17 00:00:00 2001 From: SiyaoZheng Date: Fri, 20 Mar 2026 20:17:21 +0800 Subject: [PATCH] fix(claude): auto-detect CLAUDE_CODE_EXECUTABLE when spawning Claude ACP agent When claude-agent-acp is spawned via acpx, it internally calls the Claude Agent SDK's query() which spawns a Claude Code CLI subprocess. If CLAUDE_CODE_EXECUTABLE is not set and isStaticBinary() returns false (non-Bun environments), pathToClaudeCodeExecutable is undefined, causing the CLI subprocess to fail immediately with "Query closed before response received". This change detects when the Claude ACP agent is being spawned and automatically resolves the claude CLI path via `which` or common install locations, setting CLAUDE_CODE_EXECUTABLE in the child process environment. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index 74edcea..3481f74 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,4 +1,9 @@ -import { spawn, type ChildProcess, type ChildProcessByStdio } from "node:child_process"; +import { + execFileSync, + spawn, + type ChildProcess, + type ChildProcessByStdio, +} from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { Readable, Writable } from "node:stream"; @@ -712,10 +717,52 @@ function maybeWrapSessionControlError( return wrapped; } +function detectClaudeCliPath(): string | undefined { + try { + const resolved = execFileSync("which", ["claude"], { + encoding: "utf8", + timeout: 2_000, + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + if (resolved && fs.existsSync(resolved)) { + return resolved; + } + } catch { + // not found in PATH + } + + // Common install locations + const candidates = [ + path.join(process.env.HOME ?? "", ".local", "bin", "claude"), + path.join(process.env.HOME ?? "", ".claude", "bin", "claude"), + "/usr/local/bin/claude", + ]; + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + + return undefined; +} + function buildAgentEnvironment( authCredentials: Record | undefined, + options?: { claudeAcp?: boolean }, ): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { ...process.env }; + + // claude-agent-acp requires CLAUDE_CODE_EXECUTABLE to locate the Claude CLI + // when it is not bundled as a static binary. Without this, the SDK's query() + // spawns a child process that cannot find the CLI and exits immediately, + // causing "Query closed before response received". + if (options?.claudeAcp && !env.CLAUDE_CODE_EXECUTABLE) { + const detected = detectClaudeCliPath(); + if (detected) { + env.CLAUDE_CODE_EXECUTABLE = detected; + } + } + if (!authCredentials) { return env; } @@ -747,6 +794,7 @@ function buildAgentEnvironment( export function buildAgentSpawnOptions( cwd: string, authCredentials: Record | undefined, + options?: { claudeAcp?: boolean }, ): { cwd: string; env: NodeJS.ProcessEnv; @@ -755,7 +803,7 @@ export function buildAgentSpawnOptions( } { return { cwd, - env: buildAgentEnvironment(authCredentials), + env: buildAgentEnvironment(authCredentials, options), stdio: ["pipe", "pipe", "pipe"], windowsHide: true, }; @@ -931,6 +979,7 @@ export class AcpClient { const args = await resolveGeminiCommandArgs(command, initialArgs); this.log(`spawning agent: ${command} ${args.join(" ")}`); const geminiAcp = isGeminiAcpCommand(command, args); + const claudeAcp = isClaudeAcpCommand(command, args); const copilotAcp = isCopilotAcpCommand(command, args); if (copilotAcp) { @@ -942,7 +991,7 @@ export class AcpClient { args, buildSpawnCommandOptions( command, - buildAgentSpawnOptions(this.options.cwd, this.options.authCredentials), + buildAgentSpawnOptions(this.options.cwd, this.options.authCredentials, { claudeAcp }), ), ) as ChildProcessByStdio;