Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
20a30c1
fix: normalize Windows CLI executable paths with missing extensions
StillKnotKnown Jan 19, 2026
322ca16
test: add fs mocking tests for normalizeExecutablePath
StillKnotKnown Jan 19, 2026
8d8211e
test: use explicit mock default instead of delegating to mocked fs
StillKnotKnown Jan 19, 2026
08510d3
test: use describeUnix for Unix-generic behavior instead of describeM…
StillKnotKnown Jan 19, 2026
059cb4d
fix: address multiple root causes of Windows CLI validation failures
StillKnotKnown Jan 19, 2026
86468ac
fix: Windows CLI path validation - consolidate platform abstraction a…
StillKnotKnown Jan 19, 2026
a938921
refactor: use platform abstraction in high-priority files
StillKnotKnown Jan 19, 2026
111c9ff
refactor: use platform abstraction in MEDIUM/HIGH priority files
StillKnotKnown Jan 19, 2026
02ff75e
refactor: complete platform abstraction for remaining files
StillKnotKnown Jan 19, 2026
8aa42a3
test: add tests for new platform utility functions
StillKnotKnown Jan 19, 2026
b513d97
fix: platform abstraction improvements and CodeRabbit feedback
StillKnotKnown Jan 19, 2026
3d8cb65
test: fix getVenvPythonPath test for cross-platform compatibility
StillKnotKnown Jan 19, 2026
52ebd6a
fix: replace direct platform checks with platform abstraction
StillKnotKnown Jan 19, 2026
c4641a5
fix: replace direct platform checks in index.ts and improve GitHub CL…
StillKnotKnown Jan 19, 2026
4b1d2e0
test: add comprehensive test suite for platform/paths.ts
StillKnotKnown Jan 19, 2026
67ea01b
test: increase timeout for flaky Windows CI test
StillKnotKnown Jan 19, 2026
518c9f5
fix: replace remaining direct process.platform checks with platform a…
StillKnotKnown Jan 19, 2026
3c741ff
fix: use expandWindowsEnvVars for Windows path detection in worktree-…
StillKnotKnown Jan 19, 2026
6bf1776
fix: add proper environment variable fallbacks for Windows terminal p…
StillKnotKnown Jan 19, 2026
945a7d5
refactor: consolidate duplicate expandWindowsEnvVars implementations
StillKnotKnown Jan 19, 2026
7644dd1
test: add comprehensive test suite for windows-paths.ts
StillKnotKnown Jan 19, 2026
d91bb44
test: add pty-manager.test.ts with 20 passing tests
StillKnotKnown Jan 19, 2026
4d557db
refactor: use environment variable expansion in COMMON_BIN_PATHS
StillKnotKnown Jan 19, 2026
22b9f11
test: fix windows-paths.test.ts - 31/35 tests now passing (89%)
StillKnotKnown Jan 19, 2026
2d1ef25
test: fix lint and TypeScript errors in test files
StillKnotKnown Jan 19, 2026
6734179
lint: fix pre-commit hook failures
StillKnotKnown Jan 19, 2026
6824658
test: fix duplicate beforeEach hook in windows-paths.test.ts
StillKnotKnown Jan 19, 2026
27f44f0
Merge branch 'develop' into windows-user-path-validation
StillKnotKnown Jan 19, 2026
8114f62
test: address CodeRabbit feedback - fake timers, async tests, path no…
StillKnotKnown Jan 19, 2026
a7b5b95
refactor: remove unused imports and variables (CodeQL alerts)
StillKnotKnown Jan 19, 2026
d3a9f17
docs: map existing codebase
Jan 19, 2026
161a9dc
docs: initialize project
Jan 19, 2026
62b80e0
chore: add project config
Jan 19, 2026
b2de1bf
Merge branch 'develop' into windows-user-path-validation
StillKnotKnown Jan 19, 2026
c3ad052
Merge branch 'develop' into windows-user-path-validation
StillKnotKnown Jan 19, 2026
26c50f0
Merge branch 'develop' into windows-user-path-validation
StillKnotKnown Jan 19, 2026
9ee39af
fix: comprehensive platform abstraction alignment for CLI path handling
StillKnotKnown Jan 19, 2026
f2ef1bb
fix: align remaining code with centralized platform abstraction
StillKnotKnown Jan 19, 2026
af5653f
fix: comprehensive platform abstraction for CLI tools and terminals
StillKnotKnown Jan 19, 2026
b67d69e
fix: resolve CodeQL and Biome linter issues
StillKnotKnown Jan 19, 2026
a5e8983
test: fix duplicate beforeEach hook linter warnings
StillKnotKnown Jan 19, 2026
a3a1520
fix: return normalized executable paths from CLI tool validation
StillKnotKnown Jan 19, 2026
7c621a2
fix: critical security and platform compatibility fixes
StillKnotKnown Jan 19, 2026
8bbd204
test: fix platform-specific test failures for cross-platform CI
StillKnotKnown Jan 19, 2026
604ae7a
test: fix biome-ignore comment syntax in platform.test.ts
StillKnotKnown Jan 19, 2026
e781c3f
fix: i18n support for error messages and linter fixes
StillKnotKnown Jan 19, 2026
a72846e
fix: comprehensive platform alignment and test coverage
StillKnotKnown Jan 19, 2026
bed2c01
fix: comprehensive CLI tool detection and platform alignment
StillKnotKnown Jan 19, 2026
88f61ff
fix: address CodeQL alerts and CI test failures
StillKnotKnown Jan 20, 2026
f774459
fix: address CodeRabbit feedback for platform abstraction and test qu…
StillKnotKnown Jan 20, 2026
6d3fce7
refactor: consolidate platform abstraction and remove duplicate helpers
StillKnotKnown Jan 20, 2026
f7a95e0
test: fix platform module mocks for getEnvVar and isSecurePath
StillKnotKnown Jan 20, 2026
022401e
test: update test files to use platform abstraction functions
StillKnotKnown Jan 20, 2026
ef2033d
test: fix platform mocks for debug-logger and remove unused import
StillKnotKnown Jan 20, 2026
5bca79f
test: add comprehensive test coverage for python-detector and path ed…
StillKnotKnown Jan 20, 2026
ebcabb4
test: fix config-path-validator test expectations and remove unused i…
StillKnotKnown Jan 20, 2026
4ec7119
fix: prevent renderer from pulling in Node.js dependencies
StillKnotKnown Jan 20, 2026
d9ce7c8
test: fix REDOS vulnerability in windows-paths.test.ts
StillKnotKnown Jan 20, 2026
da1a080
fix: comprehensive platform abstraction alignment improvements
StillKnotKnown Jan 20, 2026
48e2714
test: fix REDOS vulnerability using backslash delimiter approach
StillKnotKnown Jan 20, 2026
25b7c35
test: fix platform-specific path handling and test expectations
StillKnotKnown Jan 20, 2026
c4ac8f8
refactor: align remaining code with centralized platform abstraction
StillKnotKnown Jan 20, 2026
d13cfee
test: fix Windows CI test failures for path handling
StillKnotKnown Jan 20, 2026
2c87bdf
test: add missing platform tests and fix skipped where.exe tests
StillKnotKnown Jan 20, 2026
4cc76d5
test: fix Windows CI test failures for platform-specific paths
StillKnotKnown Jan 20, 2026
f316f3f
fix: security and platform alignment improvements from PR review
StillKnotKnown Jan 20, 2026
fbbe37f
test: fix Windows CI Homebrew Python mock
StillKnotKnown Jan 20, 2026
2fc5711
feat: add GitLab CLI paths and Linux Python paths to platform module
StillKnotKnown Jan 20, 2026
4fae75f
refactor: use getBinaryDirectories() instead of hardcoded /usr/bin paths
StillKnotKnown Jan 20, 2026
74a1c57
refactor: use getEnvVar for case-insensitive HOME access in renderer
StillKnotKnown Jan 20, 2026
1189694
Merge branch 'develop' into windows-user-path-validation
StillKnotKnown Jan 20, 2026
f098f95
refactor: remove unused os and execSync imports
StillKnotKnown Jan 20, 2026
8464554
test: add comprehensive tests for configureTools, clearToolCache, and…
StillKnotKnown Jan 20, 2026
767de21
fix: use getEnvVar() for case-insensitive Windows env access
StillKnotKnown Jan 20, 2026
b3b2aaa
feat: comprehensive platform alignment for CLI path detection
StillKnotKnown Jan 20, 2026
f02b3a8
test: fix gh_executable and git_executable test failures
StillKnotKnown Jan 20, 2026
63d3c39
test: fix gh_executable and git_executable test failures
StillKnotKnown Jan 20, 2026
0159729
feat: add GitLab CLI env var support and Git executable verification
StillKnotKnown Jan 20, 2026
f08c40a
test: fix cache pollution in test_checks_homebrew_paths_on_unix
StillKnotKnown Jan 20, 2026
9c5a475
feat: add full GitLab CLI detection support to frontend
StillKnotKnown Jan 20, 2026
008f6d8
test: fix CodeQL URL substring sanitization alert
StillKnotKnown Jan 20, 2026
0256919
fix: platform abstraction improvements and install script bug fix
StillKnotKnown Jan 20, 2026
db3f2d1
test: fix Windows CI test failures with path normalization and async …
StillKnotKnown Jan 20, 2026
5479fdb
test: fix Windows CI test failures with async timing and mock resets
StillKnotKnown Jan 20, 2026
5a74d20
test: fix homebrew-python mock reset issue for cross-platform CI
StillKnotKnown Jan 20, 2026
04a3e90
test: normalize paths in homebrew-python mock for cross-platform CI
StillKnotKnown Jan 20, 2026
e4cadf0
test: normalize mockValidate paths for Windows cross-platform tests
StillKnotKnown Jan 20, 2026
a2674fe
test: use vi.waitFor for event emission tests reliability
StillKnotKnown Jan 20, 2026
4a4194f
feat: add bundled Python paths to security allowlist
StillKnotKnown Jan 20, 2026
5351f3f
fix: add process polyfill for renderer in dev mode
StillKnotKnown Jan 20, 2026
b525b91
fix: use planner agent type for ideation to prevent stream closure
StillKnotKnown Jan 20, 2026
d18d2d8
feat: add comprehensive Claude CLI detection paths for Windows
StillKnotKnown Jan 20, 2026
f91260e
fix: use explicit where.exe with proper line ending handling
StillKnotKnown Jan 20, 2026
ba5ef0d
fix: use explicit commands and proper line ending handling for cross-…
StillKnotKnown Jan 20, 2026
63b8e71
fix: use getCmdExecutablePath() for cmd.exe resolution in claude-code…
StillKnotKnown Jan 20, 2026
186a539
fix: search multiple extensions in where.exe for Windows Claude CLI d…
StillKnotKnown Jan 20, 2026
f1f22f8
fix: use vi.waitFor for reliable async timing in kill task test
StillKnotKnown Jan 20, 2026
c7394ea
fix: add CLAUDE_CLI_PATH detection to ideation and roadmap spawns
StillKnotKnown Jan 20, 2026
6791090
refactor: use centralized getToolInfo helper for CLI detection in ide…
StillKnotKnown Jan 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import {
} from '../cli-tool-manager';
import {
findWindowsExecutableViaWhere,
findWindowsExecutableViaWhereAsync,
isSecurePath
findWindowsExecutableViaWhereAsync
} from '../utils/windows-paths';
import { findExecutable, findExecutableAsync } from '../env-utils';
import { isSecurePath } from '../platform';

type SpawnOptions = Parameters<(typeof import('../env-utils'))['getSpawnOptions']>[1];
type MockDirent = import('fs').Dirent<import('node:buffer').NonSharedBuffer>;
Expand Down Expand Up @@ -169,6 +169,15 @@ vi.mock('../utils/windows-paths', () => ({
WINDOWS_GIT_PATHS: {}
}));

// Mock platform module - preserve original implementations but mock isSecurePath
vi.mock('../platform', async () => {
const actualPlatform = await vi.importActual<typeof import('../platform')>('../platform');
return {
...actualPlatform,
isSecurePath: vi.fn(() => true)
};
});

describe('cli-tool-manager - Claude CLI NVM detection', () => {
beforeEach(() => {
vi.clearAllMocks();
Expand Down Expand Up @@ -717,16 +726,24 @@ describe('cli-tool-manager - Claude CLI Windows where.exe detection', () => {

// Simulate where.exe returning path with .cmd extension (preferred over no extension)
vi.mocked(findWindowsExecutableViaWhere).mockReturnValue(
'D:\\Program Files\\nvm4w\\nodejs\\claude.cmd'
'D:\\\\Program Files\\\\nvm4w\\\\nodejs\\\\claude.cmd'
);

vi.mocked(existsSync).mockReturnValue(true);
// Mock existsSync to only return true for the where.exe path
vi.mocked(existsSync).mockImplementation((filePath) => {
const pathStr = String(filePath);
// Only the where.exe result should exist
if (pathStr.includes('nvm4w') && pathStr.includes('claude.cmd')) {
return true;
}
return false;
});
vi.mocked(execFileSync).mockReturnValue('claude-code version 1.0.0\n');

const result = getToolInfo('claude');

expect(result.found).toBe(true);
expect(result.path).toBe('D:\\Program Files\\nvm4w\\nodejs\\claude.cmd');
expect(result.path).toBe('D:\\\\Program Files\\\\nvm4w\\\\nodejs\\\\claude.cmd');
expect(result.path).toMatch(/\.(cmd|exe)$/i);
});

Expand Down
6 changes: 3 additions & 3 deletions apps/frontend/src/main/agent/agent-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type { AppSettings } from '../../shared/types/settings';
import { getOAuthModeClearVars } from './env-utils';
import { getAugmentedEnv } from '../env-utils';
import { getToolInfo } from '../cli-tool-manager';
import { killProcessGracefully } from '../platform';
import { killProcessGracefully, isWindows } from '../platform';

/**
* Type for supported CLI tools
Expand All @@ -42,7 +42,7 @@ const CLI_TOOL_ENV_MAP: Readonly<Record<CliTool, string>> = {


function deriveGitBashPath(gitExePath: string): string | null {
if (process.platform !== 'win32') {
if (!isWindows()) {
return null;
}

Expand Down Expand Up @@ -165,7 +165,7 @@ export class AgentProcessManager {
// On Windows, detect and pass git-bash path for Claude Code CLI
// Electron can detect git via where.exe, but Python subprocess may not have the same PATH
const gitBashEnv: Record<string, string> = {};
if (process.platform === 'win32' && !process.env.CLAUDE_CODE_GIT_BASH_PATH) {
if (isWindows() && !process.env.CLAUDE_CODE_GIT_BASH_PATH) {
try {
const gitInfo = getToolInfo('git');
if (gitInfo.found && gitInfo.path) {
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/main/agent/agent-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { pythonEnvManager } from '../python-env-manager';
import { transformIdeaFromSnakeCase, transformSessionFromSnakeCase } from '../ipc-handlers/ideation/transformers';
import { transformRoadmapFromSnakeCase } from '../ipc-handlers/roadmap/transformers';
import type { RawIdea } from '../ipc-handlers/ideation/types';
import { getPathDelimiter } from '../platform';

/** Maximum length for status messages displayed in progress UI */
const STATUS_MESSAGE_MAX_LENGTH = 200;
Expand Down Expand Up @@ -280,7 +281,7 @@ export class AgentQueueManager {
if (autoBuildSource) {
pythonPathParts.push(autoBuildSource);
}
const combinedPythonPath = pythonPathParts.join(process.platform === 'win32' ? ';' : ':');
const combinedPythonPath = pythonPathParts.join(getPathDelimiter());

// Build final environment with proper precedence:
// 1. process.env (system)
Expand Down Expand Up @@ -607,7 +608,7 @@ export class AgentQueueManager {
if (autoBuildSource) {
pythonPathParts.push(autoBuildSource);
}
const combinedPythonPath = pythonPathParts.join(process.platform === 'win32' ? ';' : ':');
const combinedPythonPath = pythonPathParts.join(getPathDelimiter());

// Build final environment with proper precedence:
// 1. process.env (system)
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/main/claude-cli-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'path';
import { getAugmentedEnv, getAugmentedEnvAsync } from './env-utils';
import { getToolPath, getToolPathAsync } from './cli-tool-manager';
import { getPathDelimiter, isWindows } from './platform';

export type ClaudeCliInvocation = {
command: string;
Expand All @@ -12,12 +13,12 @@ function ensureCommandDirInPath(command: string, env: Record<string, string>): R
return env;
}

const pathSeparator = process.platform === 'win32' ? ';' : ':';
const pathSeparator = getPathDelimiter();
const commandDir = path.dirname(command);
const currentPath = env.PATH || '';
const pathEntries = currentPath.split(pathSeparator);
const normalizedCommandDir = path.normalize(commandDir);
const hasCommandDir = process.platform === 'win32'
const hasCommandDir = isWindows()
? pathEntries
.map((entry) => path.normalize(entry).toLowerCase())
.includes(normalizedCommandDir.toLowerCase())
Expand Down
98 changes: 64 additions & 34 deletions apps/frontend/src/main/cli-tool-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import os from 'os';
import { promisify } from 'util';
import { app } from 'electron';
import { findExecutable, findExecutableAsync, getAugmentedEnv, getAugmentedEnvAsync, shouldUseShell, existsAsync } from './env-utils';
import { isWindows, isMacOS, isUnix, joinPaths, getExecutableExtension } from './platform';
import { isWindows, isMacOS, isUnix, joinPaths, getExecutableExtension, normalizeExecutablePath, isSecurePath as isPathSecure } from './platform';
import type { ToolDetectionResult } from '../shared/types';
import { findHomebrewPython as findHomebrewPythonUtil } from './utils/homebrew-python';

Expand All @@ -48,7 +48,6 @@ import {
WINDOWS_GIT_PATHS,
findWindowsExecutableViaWhere,
findWindowsExecutableViaWhereAsync,
isSecurePath,
} from './utils/windows-paths';

/**
Expand Down Expand Up @@ -96,25 +95,32 @@ interface CacheEntry {
function isWrongPlatformPath(pathStr: string | undefined): boolean {
if (!pathStr) return false;

// Strip quotes before platform check - quotes are handled by validation
let cleanPath = pathStr.trim();
if ((cleanPath.startsWith('"') && cleanPath.endsWith('"')) ||
(cleanPath.startsWith("'") && cleanPath.endsWith("'"))) {
cleanPath = cleanPath.slice(1, -1);
}

if (isWindows()) {
// On Windows, reject Unix-style absolute paths (starting with /)
// but allow relative paths and Windows paths
if (pathStr.startsWith('/') && !pathStr.startsWith('//')) {
if (cleanPath.startsWith('/') && !cleanPath.startsWith('//')) {
// Unix absolute path on Windows
return true;
}
} else {
// On Unix (macOS/Linux), reject Windows-style paths
// Windows paths have: drive letter (C:), backslashes, or specific Windows paths
if (/^[A-Za-z]:[/\\]/.test(pathStr)) {
if (/^[A-Za-z]:[/\\]/.test(cleanPath)) {
// Drive letter path (C:\, D:/, etc.)
return true;
}
if (pathStr.includes('\\')) {
if (cleanPath.includes('\\')) {
// Contains backslashes (Windows path separators)
return true;
}
if (pathStr.includes('AppData') || pathStr.includes('Program Files')) {
if (cleanPath.includes('AppData') || cleanPath.includes('Program Files')) {
// Contains Windows-specific directory names
return true;
}
Expand Down Expand Up @@ -724,17 +730,25 @@ class CLIToolManager {
console.warn(
`[Claude CLI] User-configured path is from different platform, ignoring: ${this.userConfig.claudePath}`
);
} else if (isWindows() && !isSecurePath(this.userConfig.claudePath)) {
console.warn(
`[Claude CLI] User-configured path failed security validation, ignoring: ${this.userConfig.claudePath}`
);
} else {
const validation = this.validateClaude(this.userConfig.claudePath);
const result = buildClaudeDetectionResult(
this.userConfig.claudePath, validation, 'user-config', 'Using user-configured Claude CLI'
);
if (result) return result;
console.warn(`[Claude CLI] User-configured path invalid: ${validation.message}`);
// Strip quotes before security check - quotes are handled by validateClaude
const unquotedPath = this.userConfig.claudePath.trim();
const cleanPath = unquotedPath.startsWith('"') && unquotedPath.endsWith('"')
? unquotedPath.slice(1, -1)
: unquotedPath;

if (isWindows() && !isPathSecure(cleanPath)) {

This comment was marked as outdated.

console.warn(
`[Claude CLI] User-configured path failed security validation, ignoring: ${cleanPath}`
);
} else {
const validation = this.validateClaude(this.userConfig.claudePath);
const result = buildClaudeDetectionResult(
this.userConfig.claudePath, validation, 'user-config', 'Using user-configured Claude CLI'
);
if (result) return result;
console.warn(`[Claude CLI] User-configured path invalid: ${validation.message}`);
}
}
}

Expand Down Expand Up @@ -940,7 +954,11 @@ class CLIToolManager {
? trimmedCmd.slice(1, -1)
: trimmedCmd;

const needsShell = shouldUseShell(trimmedCmd);
// Normalize the path on Windows to handle missing extensions
// e.g., C:\...\npm\claude -> C:\...\npm\claude.cmd
const normalizedCmd = normalizeExecutablePath(unquotedCmd);

const needsShell = shouldUseShell(normalizedCmd);
const cmdDir = path.dirname(unquotedCmd);
const env = getAugmentedEnv(cmdDir && cmdDir !== '.' ? [cmdDir] : []);

Expand All @@ -949,15 +967,15 @@ class CLIToolManager {
if (needsShell) {
// For .cmd/.bat files on Windows, use cmd.exe with a quoted command line
// /s preserves quotes so paths with spaces are handled correctly.
if (!isSecurePath(unquotedCmd)) {
if (!isPathSecure(normalizedCmd)) {
return {
valid: false,
message: `Claude CLI path failed security validation: ${unquotedCmd}`,
};
}
const cmdExe = process.env.ComSpec
|| path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'cmd.exe');
const cmdLine = `""${unquotedCmd}" --version"`;
const cmdLine = `""${normalizedCmd}" --version"`;
const execOptions: ExecFileSyncOptionsWithVerbatim = {
encoding: 'utf-8',
timeout: 5000,
Expand All @@ -971,7 +989,7 @@ class CLIToolManager {
} else {
// For .exe files and non-Windows, use execFileSync
version = normalizeExecOutput(
execFileSync(unquotedCmd, ['--version'], {
execFileSync(normalizedCmd, ['--version'], {
encoding: 'utf-8',
timeout: 5000,
windowsHide: true,
Expand Down Expand Up @@ -1079,7 +1097,11 @@ class CLIToolManager {
? trimmedCmd.slice(1, -1)
: trimmedCmd;

const needsShell = shouldUseShell(trimmedCmd);
// Normalize the path on Windows to handle missing extensions
// e.g., C:\...\npm\claude -> C:\...\npm\claude.cmd
const normalizedCmd = normalizeExecutablePath(unquotedCmd);

const needsShell = shouldUseShell(normalizedCmd);
const cmdDir = path.dirname(unquotedCmd);
const env = await getAugmentedEnvAsync(cmdDir && cmdDir !== '.' ? [cmdDir] : []);

Expand All @@ -1088,15 +1110,15 @@ class CLIToolManager {
if (needsShell) {
// For .cmd/.bat files on Windows, use cmd.exe with a quoted command line
// /s preserves quotes so paths with spaces are handled correctly.
if (!isSecurePath(unquotedCmd)) {
if (!isPathSecure(normalizedCmd)) {
return {
valid: false,
message: `Claude CLI path failed security validation: ${unquotedCmd}`,
};
}
const cmdExe = process.env.ComSpec
|| path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'cmd.exe');
const cmdLine = `""${unquotedCmd}" --version"`;
const cmdLine = `""${normalizedCmd}" --version"`;
const execOptions: ExecFileAsyncOptionsWithVerbatim = {
encoding: 'utf-8',
timeout: 5000,
Expand All @@ -1108,7 +1130,7 @@ class CLIToolManager {
stdout = result.stdout;
} else {
// For .exe files and non-Windows, use execFileAsync
const result = await execFileAsync(unquotedCmd, ['--version'], {
const result = await execFileAsync(normalizedCmd, ['--version'], {
encoding: 'utf-8',
timeout: 5000,
windowsHide: true,
Expand Down Expand Up @@ -1280,17 +1302,25 @@ class CLIToolManager {
console.warn(
`[Claude CLI] User-configured path is from different platform, ignoring: ${this.userConfig.claudePath}`
);
} else if (isWindows() && !isSecurePath(this.userConfig.claudePath)) {
console.warn(
`[Claude CLI] User-configured path failed security validation, ignoring: ${this.userConfig.claudePath}`
);
} else {
const validation = await this.validateClaudeAsync(this.userConfig.claudePath);
const result = buildClaudeDetectionResult(
this.userConfig.claudePath, validation, 'user-config', 'Using user-configured Claude CLI'
);
if (result) return result;
console.warn(`[Claude CLI] User-configured path invalid: ${validation.message}`);
// Strip quotes before security check - quotes are handled by validateClaudeAsync
const unquotedPath = this.userConfig.claudePath.trim();
const cleanPath = unquotedPath.startsWith('"') && unquotedPath.endsWith('"')
? unquotedPath.slice(1, -1)
: unquotedPath;

if (isWindows() && !isPathSecure(cleanPath)) {
console.warn(
`[Claude CLI] User-configured path failed security validation, ignoring: ${cleanPath}`
);
} else {
const validation = await this.validateClaudeAsync(this.userConfig.claudePath);
const result = buildClaudeDetectionResult(
this.userConfig.claudePath, validation, 'user-config', 'Using user-configured Claude CLI'
);
if (result) return result;
console.warn(`[Claude CLI] User-configured path invalid: ${validation.message}`);
}
}
}

Expand Down
Loading