diff --git a/src/utils/Shell.ts b/src/utils/Shell.ts index d0097e027..99e799c48 100644 --- a/src/utils/Shell.ts +++ b/src/utils/Shell.ts @@ -72,6 +72,42 @@ function isExecutable(shellPath: string): boolean { * Determines the best available shell to use. */ export async function findSuitableShell(): Promise { + // Windows-only one-shot warning: if WSL bash launcher is on PATH and no + // Git for Windows bash is detected, surface a warning so the user knows + // why hooks/BashTool will misbehave. The actual filtering lives in + // findExecutableWithDeps (windowsPaths.ts); this is purely user-facing. + if ( + getPlatform() === 'windows' && + !process.env.CLAUDE_CODE_GIT_BASH_PATH && + !process.env.CLAUDE_CODE_GIT_BASH_PATH_WARNED + ) { + try { + const whereResult = execFileSync('where.exe', ['bash'], { + stdio: ['ignore', 'pipe', 'ignore'], + encoding: 'utf8', + }) + const lines = whereResult + .split(/\r?\n/) + .map(l => l.trim().toLowerCase()) + .filter(Boolean) + const hasWslBash = lines.some(l => + /(?:system32|windowsapps)\\bash\.exe$/.test(l), + ) + const hasGitBash = lines.some(l => /\\git\\.*bash\.exe$/.test(l)) + if (hasWslBash && !hasGitBash) { + process.env.CLAUDE_CODE_GIT_BASH_PATH_WARNED = '1' + console.warn( + '[CCB] Detected WSL bash on PATH without Git for Windows. ' + + 'Hooks and BashTool will not work correctly. ' + + 'Install Git for Windows (https://git-scm.com/download/windows) ' + + 'or set CLAUDE_CODE_GIT_BASH_PATH to your bash.exe.', + ) + } + } catch { + // where.exe not available or failed — fall through silently + } + } + // Check for explicit shell override first const shellOverride = process.env.CLAUDE_CODE_SHELL if (shellOverride) { diff --git a/src/utils/__tests__/windowsPaths.test.ts b/src/utils/__tests__/windowsPaths.test.ts index 340eb3ac9..ecb6e9232 100644 --- a/src/utils/__tests__/windowsPaths.test.ts +++ b/src/utils/__tests__/windowsPaths.test.ts @@ -322,4 +322,55 @@ describe('findGitBashPathOrNullWithDeps', () => { const result = findGitBashPathOrNullWithDeps(deps) expect(result).toBe(expectedBash) }) + + test('rejects WSL bash launcher (System32) and returns null when no Git Bash exists', () => { + // WSL feature on Windows ships C:\Windows\System32\bash.exe — a Linux + // distro launcher, NOT Git Bash. It must be rejected; if no Git for + // Windows exists, return null so findGitBashPath can exit(1) cleanly. + const wslBash = 'C:\\Windows\\System32\\bash.exe' + const deps: GitBashDiscoveryDeps = { + // Only the WSL launcher "exists" — Git Bash default locations do not, + // so step 4 fallback (searchDefaultBashLocations) also returns null. + checkExists: p => p === wslBash, + execCommand: cmd => (cmd.includes('where.exe bash') ? wslBash : ''), + cwdFn: () => 'C:\\safe\\cwd', + envOverride: '', + } + expect(findGitBashPathOrNullWithDeps(deps)).toBe(null) + }) + + test('rejects WSL App Execution Alias (WindowsApps) and returns null when no Git Bash exists', () => { + // %LOCALAPPDATA%\Microsoft\WindowsApps\bash.exe is the WSL App Execution + // Alias — a 0-byte reparse point that forwards to WSL. Same rejection. + const wslBash = + 'C:\\Users\\foo\\AppData\\Local\\Microsoft\\WindowsApps\\bash.exe' + const deps: GitBashDiscoveryDeps = { + checkExists: p => p === wslBash, + execCommand: cmd => (cmd.includes('where.exe bash') ? wslBash : ''), + cwdFn: () => 'C:\\safe\\cwd', + envOverride: '', + } + expect(findGitBashPathOrNullWithDeps(deps)).toBe(null) + }) + + test('skips WSL bash and falls through to next where.exe hit (Git Bash later in PATH)', () => { + // Common case: user has WSL enabled AND Git for Windows installed. + // where.exe returns System32\bash.exe first (System32 is near front of + // PATH), then Git Bash later. Filter must skip the WSL entry and accept + // the legit one. + const wslBash = 'C:\\Windows\\System32\\bash.exe' + const legitBash = 'C:\\Program Files\\Git\\bin\\bash.exe' + const deps: GitBashDiscoveryDeps = { + checkExists: p => p === legitBash || p === wslBash, + execCommand: cmd => { + if (cmd.includes('where.exe bash')) { + return `${wslBash}\r\n${legitBash}` + } + return '' + }, + cwdFn: () => 'C:\\safe\\cwd', + envOverride: '', + } + expect(findGitBashPathOrNullWithDeps(deps)).toBe(legitBash) + }) }) diff --git a/src/utils/doctorDiagnostic.ts b/src/utils/doctorDiagnostic.ts index f4610c4e9..15c22c0c1 100644 --- a/src/utils/doctorDiagnostic.ts +++ b/src/utils/doctorDiagnostic.ts @@ -364,6 +364,33 @@ async function detectConfigurationIssues( // Parse errors are surfaced by the settings loader itself. } + // Windows-only: detect WSL bash launcher on PATH without Git for Windows. + // Independent of install method — affects every Windows user with WSL. + if (getPlatform() === 'windows') { + try { + const whereResult = await execFileNoThrow('where.exe', ['bash']) + if (whereResult.code === 0 && whereResult.stdout) { + const lines = whereResult.stdout + .split(/\r?\n/) + .map(l => l.trim().toLowerCase()) + .filter(Boolean) + const hasWslBash = lines.some(l => + /(?:system32|windowsapps)\\bash\.exe$/.test(l), + ) + const hasGitBash = lines.some(l => /\\git\\.*bash\.exe$/.test(l)) + if (hasWslBash && !hasGitBash) { + warnings.push({ + issue: + 'Windows PATH has WSL bash (C:\\Windows\\System32\\bash.exe) but no Git for Windows bash', + fix: 'Install Git for Windows (https://git-scm.com/download/windows). Without it, CCB cannot run hooks or BashTool correctly on Windows. If you cannot install it, set CLAUDE_CODE_GIT_BASH_PATH to a working bash.exe.', + }) + } + } + } catch { + // where.exe not available or failed — skip this check + } + } + const config = getGlobalConfig() // Skip most warnings for development mode diff --git a/src/utils/windowsPaths.ts b/src/utils/windowsPaths.ts index 165379538..770b64116 100644 --- a/src/utils/windowsPaths.ts +++ b/src/utils/windowsPaths.ts @@ -157,6 +157,23 @@ function findExecutableWithDeps( continue } + // WSL-bash rejection: System32\bash.exe (WSL launcher) and + // WindowsApps\bash.exe (WSL App Execution Alias) must never be + // accepted as Git Bash. They look like bash but spawn wsl.exe, + // causing popup windows and Linux-semantics commands on Windows + // tasks. Only applies to bash.exe, not other executables looked + // up here (git, etc.). Continue to the next where.exe hit so a + // legit Git Bash later in PATH can still win. + if ( + executable === 'bash' && + /(?:system32|windowsapps)\\bash\.exe$/.test(normalizedPath) + ) { + logForDebugging( + `Skipping WSL bash launcher (not Git Bash): ${candidatePath}`, + ) + continue + } + // Return the first valid path that's not in the current directory return candidatePath }