diff --git a/cli/src/claude/claudeLocal.ts b/cli/src/claude/claudeLocal.ts index bce6904e..5e3e27cf 100644 --- a/cli/src/claude/claudeLocal.ts +++ b/cli/src/claude/claudeLocal.ts @@ -9,6 +9,7 @@ import { withBunRuntimeEnv } from "@/utils/bunRuntime"; import { spawnWithAbort } from "@/utils/spawnWithAbort"; import { getHapiBlobsDir } from "@/constants/uploadPaths"; import { stripNewlinesForWindowsShellArg } from "@/utils/shellEscape"; +import { getDefaultClaudeCodePath } from "./sdk/utils"; export async function claudeLocal(opts: { abort: AbortSignal, @@ -84,11 +85,15 @@ export async function claudeLocal(opts: { logger.debug(`[ClaudeLocal] Spawning claude with args: ${JSON.stringify(args)}`); + // Get Claude executable path (absolute path on Windows for shell: false) + const claudeCommand = getDefaultClaudeCodePath(); + logger.debug(`[ClaudeLocal] Using claude executable: ${claudeCommand}`); + // Spawn the process try { process.stdin.pause(); await spawnWithAbort({ - command: 'claude', + command: claudeCommand, args, cwd: opts.path, env: withBunRuntimeEnv(env, { allowBunBeBun: false }), @@ -98,7 +103,7 @@ export async function claudeLocal(opts: { installHint: 'Claude CLI', includeCause: true, logExit: true, - shell: process.platform === 'win32' + shell: false // Use absolute path, no shell needed }); } finally { cleanupMcpConfig?.(); diff --git a/cli/src/claude/sdk/query.ts b/cli/src/claude/sdk/query.ts index bf443860..725cb3ba 100644 --- a/cli/src/claude/sdk/query.ts +++ b/cli/src/claude/sdk/query.ts @@ -347,8 +347,9 @@ export function query(config: { stdio: ['pipe', 'pipe', 'pipe'], signal: config.options?.abort, env: spawnEnv, - // Use shell on Windows for command resolution - shell: process.platform === 'win32' + // Use shell: false with absolute path from getDefaultClaudeCodePath() + // This avoids cmd.exe resolution issues on Windows + shell: false }) as ChildProcessWithoutNullStreams // Handle stdin diff --git a/cli/src/claude/sdk/utils.ts b/cli/src/claude/sdk/utils.ts index a24d4371..e3518d1b 100644 --- a/cli/src/claude/sdk/utils.ts +++ b/cli/src/claude/sdk/utils.ts @@ -8,19 +8,64 @@ import { execSync } from 'node:child_process' import { homedir } from 'node:os' import { logger } from '@/ui/logger' +/** + * Find Claude executable path on Windows. + * Returns absolute path to claude.exe for use with shell: false + */ +function findWindowsClaudePath(): string | null { + const homeDir = homedir() + const path = require('node:path') + + // Known installation paths for Claude on Windows + const candidates = [ + path.join(homeDir, '.local', 'bin', 'claude.exe'), + path.join(homeDir, 'AppData', 'Local', 'Programs', 'claude', 'claude.exe'), + path.join(homeDir, 'AppData', 'Local', 'Microsoft', 'WinGet', 'Packages', 'Anthropic.claude-code_Microsoft.Winget.Source_8wekyb3d8bbwe', 'claude.exe'), + ] + + for (const candidate of candidates) { + if (existsSync(candidate)) { + logger.debug(`[Claude SDK] Found Windows claude.exe at: ${candidate}`) + return candidate + } + } + + // Try 'where claude' to find in PATH + try { + const result = execSync('where claude.exe', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + cwd: homeDir + }).trim().split('\n')[0].trim() + if (result && existsSync(result)) { + logger.debug(`[Claude SDK] Found Windows claude.exe via where: ${result}`) + return result + } + } catch { + // where didn't find it + } + + return null +} + /** * Try to find globally installed Claude CLI - * Returns 'claude' if the command works globally (preferred method for reliability) - * Falls back to which/where to get actual path on Unix systems + * On Windows: Returns absolute path to claude.exe (for shell: false) + * On Unix: Returns 'claude' if command works, or actual path via which * Runs from home directory to avoid local cwd side effects */ function findGlobalClaudePath(): string | null { const homeDir = homedir() - - // PRIMARY: Check if 'claude' command works directly from home dir + + // Windows: Always return absolute path for shell: false compatibility + if (process.platform === 'win32') { + return findWindowsClaudePath() + } + + // Unix: Check if 'claude' command works directly from home dir try { - execSync('claude --version', { - encoding: 'utf8', + execSync('claude --version', { + encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], cwd: homeDir }) @@ -31,22 +76,20 @@ function findGlobalClaudePath(): string | null { } // FALLBACK for Unix: try which to get actual path - if (process.platform !== 'win32') { - try { - const result = execSync('which claude', { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'], - cwd: homeDir - }).trim() - if (result && existsSync(result)) { - logger.debug(`[Claude SDK] Found global claude path via which: ${result}`) - return result - } - } catch { - // which didn't find it + try { + const result = execSync('which claude', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + cwd: homeDir + }).trim() + if (result && existsSync(result)) { + logger.debug(`[Claude SDK] Found global claude path via which: ${result}`) + return result } + } catch { + // which didn't find it } - + return null }