fix(frontend): add windowsVerbatimArguments for Windows .cmd validation (ACS-252) (#1075)

* fix(frontend): add windowsVerbatimArguments for Windows .cmd validation

Fixes installation scan failures on Windows when Claude Code CLI is
installed via npm in paths containing spaces (e.g., nvm4w).

The validateClaudeCliAsync function in claude-code-handlers.ts was
missing the windowsVerbatimArguments: true option when executing
.cmd files via cmd.exe, causing validation failures for paths like
"D:\Program Files\nvm4w\nodejs\claude.cmd".

This aligns the implementation with the working pattern already used
in cli-tool-manager.ts validateClaudeAsync().

Changes:
- Add ExecFileAsyncOptionsWithVerbatim type definition
- Set windowsVerbatimArguments: true in execOptions for .cmd files

Refs: ACS-252

* refactor: address PR review feedback

- Export ExecFileAsyncOptionsWithVerbatim from cli-tool-manager.ts
  to avoid duplication (DRY principle)
- Import type in claude-code-handlers.ts instead of redefining
- Add isSecurePath validation in validateClaudeCliAsync for security

Addresses review comments on PR #1075

* refactor: use top-level type imports for better consistency

Replace inline import('child_process') type imports with top-level
type imports from 'child_process' module for better code style
consistency with other imports in the file.

Addresses CodeRabbit nitpick suggestion on PR #1075.

---------

Co-authored-by: StillKnotKnown <stillknotknown@users.noreply.github.com>
Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
StillKnotKnown
2026-01-15 22:42:54 +02:00
committed by GitHub
parent 2eef82bf2a
commit 658f26cb47
2 changed files with 13 additions and 6 deletions
+3 -3
View File
@@ -20,7 +20,7 @@
* - Graceful fallbacks when tools not found
*/
import { execFileSync, execFile } from 'child_process';
import { execFileSync, execFile, type ExecFileOptionsWithStringEncoding, type ExecFileSyncOptions } from 'child_process';
import { existsSync, readdirSync, promises as fsPromises } from 'fs';
import path from 'path';
import os from 'os';
@@ -32,10 +32,10 @@ import { findHomebrewPython as findHomebrewPythonUtil } from './utils/homebrew-p
const execFileAsync = promisify(execFile);
type ExecFileSyncOptionsWithVerbatim = import('child_process').ExecFileSyncOptions & {
export type ExecFileSyncOptionsWithVerbatim = ExecFileSyncOptions & {
windowsVerbatimArguments?: boolean;
};
type ExecFileAsyncOptionsWithVerbatim = import('child_process').ExecFileOptionsWithStringEncoding & {
export type ExecFileAsyncOptionsWithVerbatim = ExecFileOptionsWithStringEncoding & {
windowsVerbatimArguments?: boolean;
};
@@ -16,7 +16,7 @@ import { promisify } from 'util';
import { IPC_CHANNELS, DEFAULT_APP_SETTINGS } from '../../shared/constants';
import type { IPCResult } from '../../shared/types';
import type { ClaudeCodeVersionInfo, ClaudeInstallationList, ClaudeInstallationInfo } from '../../shared/types/cli';
import { getToolInfo, configureTools, sortNvmVersionDirs, getClaudeDetectionPaths } from '../cli-tool-manager';
import { getToolInfo, configureTools, sortNvmVersionDirs, getClaudeDetectionPaths, type ExecFileAsyncOptionsWithVerbatim } from '../cli-tool-manager';
import { readSettingsFile, writeSettingsFile } from '../settings-utils';
import { isSecurePath } from '../utils/windows-paths';
import semver from 'semver';
@@ -38,6 +38,11 @@ async function validateClaudeCliAsync(cliPath: string): Promise<[boolean, string
try {
const isWindows = process.platform === 'win32';
// Security validation: reject paths with shell metacharacters or directory traversal
if (isWindows && !isSecurePath(cliPath)) {
throw new Error(`Claude CLI path failed security validation: ${cliPath}`);
}
// Augment PATH with the CLI directory for proper resolution
const cliDir = path.dirname(cliPath);
const env = {
@@ -56,12 +61,14 @@ async function validateClaudeCliAsync(cliPath: string): Promise<[boolean, string
|| path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'cmd.exe');
// Use double-quoted command line for paths with spaces
const cmdLine = `""${cliPath}" --version"`;
const result = await execFileAsync(cmdExe, ['/d', '/s', '/c', cmdLine], {
const execOptions: ExecFileAsyncOptionsWithVerbatim = {
encoding: 'utf-8',
timeout: 5000,
windowsHide: true,
windowsVerbatimArguments: true,
env,
});
};
const result = await execFileAsync(cmdExe, ['/d', '/s', '/c', cmdLine], execOptions);
stdout = result.stdout;
} else {
const result = await execFileAsync(cliPath, ['--version'], {