Skip to content

Conversation

@g1331
Copy link
Contributor

@g1331 g1331 commented Jan 8, 2026

Description

This PR fixes Claude CLI detection issues on Windows when the installation path contains spaces (e.g., ).

Problem

Users with Claude CLI installed via nvm-windows or in paths containing spaces experienced two issues:

  1. Detection failure: Claude CLI in non-standard paths (like nvm-windows) was not detected
  2. Execution failure: Even when detected, paths with spaces failed with error:

Solution

This PR implements a two-layer solution:

Layer 1: Detection (Commits 1-2)

  • Commit 1: Add \ as 4th fallback detection step
    • Searches all PATH directories + registry entries
    • Works regardless of installation location
  • Commit 2: Prefer .cmd/.exe extensions when multiple paths found
    • Improves accuracy when \ returns multiple results

Layer 2: Execution (Commit 3)

  • Root cause: Node.js \ cannot handle space-containing paths in .cmd/.bat files, even with - Fix: Use \ with quoted paths for .cmd/.bat files, \ for .exe files
  • Implementation:
    • Add \ utility to detect .cmd/.bat files
    • Add \ utility for spawn calls
    • Use \ + quoted paths for validation
    • Add 15 comprehensive unit tests

Technical Details

Method .cmd/.bat .exe Spaces in Path
execFileSync + shell:false
execFileSync + shell:true
execSync + quoted path
spawn + getSpawnOptions()

Files Changed

  • : where.exe detection + extension preference
  • : execSync/execAsync execution strategy
  • : shouldUseShell() + getSpawnOptions() utilities
  • : spawn call updates
  • : 578 lines test enhancements
  • : 200 lines new tests (15 test cases)

Testing

✅ All existing tests pass
✅ 15 new unit tests added
✅ Manual testing with nvm-windows installation
✅ TypeScript type checking passes

Related Issue

Fixes issue where Claude CLI in paths like \ fails with error: 'D:\Program' is not recognized as internal or external command.

Type of Change

  • 🐛 Bug fix

Area

  • Frontend

Checklist

  • I've synced with \ branch
  • I've tested my changes locally
  • I've followed the code principles (SOLID, DRY, KISS)
  • My PR is small and focused (< 400 lines ideally)

CI/Testing Requirements

  • All CI checks pass
  • All existing tests pass
  • New features include test coverage
  • Bug fixes include regression tests

Breaking Changes

Breaking: No

Summary by CodeRabbit

  • New Features

    • Added async CLI detection and a spawn-options helper that auto-selects shell usage for platform-specific commands; improved Windows executable discovery priority.
  • Bug Fixes

    • More reliable cross-platform path handling (extensions, separators, spaces), NVM-aware detection ordering, and stronger validation for configured tool paths.
  • Tests

    • Expanded unit tests and mocks for sync/async flows, Windows/Unix scenarios, where.exe detection, and additional error paths.
  • Chores

    • Test environment: added headless timing API mocks for stable UI-related tests.

✏️ Tip: You can customize this high-level summary in your review settings.

@CLAassistant
Copy link

CLAassistant commented Jan 8, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

Adds async tool-detection APIs and flows for Claude CLI, including Windows where.exe (sync+async) lookup, NVM-based Unix scanning, shell-aware spawn option utilities, exported async/fs helpers, updated Windows path security/prioritization, and expanded cross-platform tests.

Changes

Cohort / File(s) Summary
Core CLI tool manager
apps/frontend/src/main/cli-tool-manager.ts
Added getToolPathAsync, async detection flow, NVM-based scanning, Windows where.exe integration (sync + async), existsAsync usage, centralized buildClaudeDetectionResult, and helpers getClaudeDetectionPaths, sortNvmVersionDirs, isPathFromWrongPlatform.
Env utilities
apps/frontend/src/main/env-utils.ts
Exported existsAsync; added shouldUseShell(command) and getSpawnOptions(command, baseOptions) controlling shell behavior; surfaced async helpers used by tests.
IPC handlers
apps/frontend/src/main/ipc-handlers/env-handlers.ts
Replaced inline spawn option objects with getSpawnOptions(...) for Claude CLI invocations.
Windows path utilities
apps/frontend/src/main/utils/windows-paths.ts
Exported isSecurePath; expanded dangerous-pattern checks; updated findWindowsExecutableViaWhere and findWindowsExecutableViaWhereAsync to prefer .cmd/.bat/.exe results.
Tests — cli-tool-manager
apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
Added extensive platform-specific and async tests, reworked mocks for child_process (sync+async), fs, env-utils; added Windows where.exe sync/async tests, NVM sorting, path fallbacks, and validation/version parsing assertions.
Tests — env-utils
apps/frontend/src/main/__tests__/env-utils.test.ts
Added tests for shouldUseShell and getSpawnOptions across Windows/non-Windows scenarios, including edge-cases and option preservation.
Tests — env handlers
apps/frontend/src/main/__tests__/env-handlers-claude-cli.test.ts
Expanded child_process test mock to include execFileSync and async-capable execFile (returns minimal ChildProcess-like object and optional callback invocation).
Test setup
apps/frontend/src/__tests__/setup.ts
Added runtime mocks for requestAnimationFrame / cancelAnimationFrame to support jsdom-based tests.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant CTM as cli-tool-manager
    participant WP as windows-paths
    participant EU as env-utils
    participant CP as child_process
    participant FS as filesystem

    App->>CTM: getToolPathAsync("claude")
    activate CTM
    CTM->>CTM: getClaudeDetectionPaths(homeDir)
    alt Windows where.exe flow
        CTM->>WP: findWindowsExecutableViaWhereAsync("claude")
        WP->>CP: execFile(where.exe ...)
        CP-->>WP: stdout (paths)
        WP->>WP: select preferred (.cmd/.bat/.exe) or first
        WP-->>CTM: candidatePath
    else Unix/NVM flow
        CTM->>FS: existsAsync("~/.nvm/versions/node/*")
        FS-->>CTM: entries
        CTM->>CTM: sortNvmVersionDirs(entries)
        CTM->>CTM: assemble candidate paths
    end
    CTM->>EU: shouldUseShell(candidatePath)
    EU-->>CTM: shell:true/false
    CTM->>CP: exec/execFile or spawn with getSpawnOptions(candidatePath,...)
    CP-->>CTM: validation stdout/exit
    CTM->>CTM: buildClaudeDetectionResult(path, validation, source, msg)
    CTM-->>App: ToolDetectionResult
    deactivate CTM
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

bug, area/frontend, size/M

Suggested reviewers

  • AlexMadera
  • AndyMik90
  • MikeeBuilds

Poem

🐰 I hopped through PATHs both near and wide,
found where.exe and NVM with pride,
added async hops and shell-aware cues,
tests mock sync and async to prove the news,
a little rabbit cheers — detection anew!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: improving Claude CLI detection on Windows with space-containing paths, which aligns with the core objective of fixing path handling for Windows installations.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @g1331, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness of Claude CLI detection and execution within the application, specifically targeting common pain points experienced by Windows users. It addresses critical issues related to non-standard installation paths (like nvm-windows) and paths containing spaces, which previously led to detection failures or execution errors. By integrating where.exe for more thorough detection and refining the command execution strategy for Windows batch files, the changes ensure a more reliable and seamless user experience across diverse Windows environments.

Highlights

  • Windows CLI Detection Improvement: Implemented a new detection layer for Claude CLI on Windows using where.exe, which can find executables even when not directly in the system PATH or when installed via nvm-windows.
  • Execution Fix for Space-Containing Paths: Resolved an issue where Claude CLI failed to execute on Windows if its installation path contained spaces, particularly for .cmd and .bat files. This was fixed by using execSync (for synchronous calls) or exec (for asynchronous calls) with quoted paths.
  • Prioritization of Executable Extensions: When where.exe returns multiple potential paths for the Claude CLI, the system now prioritizes paths ending with .cmd or .exe extensions to ensure the correct executable is selected.
  • New Utility Functions for Shell Execution: Introduced shouldUseShell and getSpawnOptions utility functions to abstract and standardize the logic for determining when a command requires shell execution on Windows, improving code clarity and maintainability.
  • Enhanced Test Coverage: Added 15 new comprehensive unit tests and enhanced existing ones (578 lines of changes) to cover the new where.exe detection logic, Windows-specific execution behaviors, and the new utility functions.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 Thanks for your first PR!

A maintainer will review it soon. Please make sure:

  • Your branch is synced with develop
  • CI checks pass
  • You've followed our contribution guide

Welcome to the Auto Claude community!

// For .cmd/.bat files on Windows, use execSync with quoted path
// execFileSync doesn't handle spaces in .cmd paths correctly even with shell:true
const quotedCmd = `"${claudeCmd}"`;
version = execSync(`${quotedCmd} --version`, {

Check warning

Code scanning / CodeQL

Indirect uncontrolled command line Medium

This command depends on an unsanitized
environment variable
.
if (needsShell) {
// For .cmd/.bat files on Windows, use exec with quoted path
const quotedCmd = `"${claudeCmd}"`;
const result = await execAsync(`${quotedCmd} --version`, {

Check warning

Code scanning / CodeQL

Indirect uncontrolled command line Medium

This command depends on an unsanitized
environment variable
.
@g1331
Copy link
Contributor Author

g1331 commented Jan 8, 2026

I have read the CLA Document and I hereby sign the CLA

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/frontend/src/main/cli-tool-manager.ts (2)

764-794: Fix duplicate step numbers in comments.

The priority step comments have duplicate "5." numbering:

  • Line 764: "5. NVM paths"
  • Line 785: "5. Platform-specific standard locations"
  • Line 794: "6. Not found"

This is inconsistent with the 6-step priority documented in the JSDoc. Line 785 should be "6. Platform-specific standard locations".

📝 Proposed fix
-    // 5. Platform-specific standard locations
+    // 6. Platform-specific standard locations
     for (const claudePath of paths.platformPaths) {

1275-1305: Same duplicate step numbering issue in async method.

Consistent with the sync method, line 1296 says "5. Platform-specific standard locations" but should be "6." to match the documented priority order.

📝 Proposed fix
-    // 5. Platform-specific standard locations
+    // 6. Platform-specific standard locations
     for (const claudePath of paths.platformPaths) {
🤖 Fix all issues with AI agents
In @apps/frontend/src/main/__tests__/cli-tool-manager.test.ts:
- Line 9: The import list currently includes unused symbols execFile and app;
remove execFile from the child_process import and either remove app from the
electron import or rename it to a prefixed identifier (e.g., import { app as
_app }) if it’s needed only for mocking; if execFile is actually used in test
mocks (see the mock setup around the test that references execFile), keep it but
prefix it (e.g., import { execFile as _execFile }) to silence the linter.

In @apps/frontend/src/main/cli-tool-manager.ts:
- Line 32: Tests fail because the child_process mock doesn't export exec which
is promisified into execAsync; update the test mock to export a mockExec
function (similar to mockExecFile) and ensure it's compatible with promisify so
that const execAsync = promisify(exec) and calls inside validateClaudeAsync use
the mocked implementation; locate the test file's mock returning { execFileSync,
execFile } and add exec: mockExec (and define mockExec to accept (cmd, opts, cb)
or (cmd, cb) pattern and call cb(null, stdout) / cb(err) as needed).
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 63e142a and 8f25c29.

📒 Files selected for processing (6)
  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
  • apps/frontend/src/main/__tests__/env-utils.test.ts
  • apps/frontend/src/main/cli-tool-manager.ts
  • apps/frontend/src/main/env-utils.ts
  • apps/frontend/src/main/ipc-handlers/env-handlers.ts
  • apps/frontend/src/main/utils/windows-paths.ts
🧰 Additional context used
📓 Path-based instructions (3)
apps/frontend/src/**/*.{ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use i18n translation keys for all user-facing text in the frontend instead of hardcoded strings

Files:

  • apps/frontend/src/main/ipc-handlers/env-handlers.ts
  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
  • apps/frontend/src/main/__tests__/env-utils.test.ts
  • apps/frontend/src/main/env-utils.ts
  • apps/frontend/src/main/cli-tool-manager.ts
  • apps/frontend/src/main/utils/windows-paths.ts
apps/frontend/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use useTranslation() hook with namespace prefixes (e.g., 'navigation:items.key') for accessing translation strings in React components

Files:

  • apps/frontend/src/main/ipc-handlers/env-handlers.ts
  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
  • apps/frontend/src/main/__tests__/env-utils.test.ts
  • apps/frontend/src/main/env-utils.ts
  • apps/frontend/src/main/cli-tool-manager.ts
  • apps/frontend/src/main/utils/windows-paths.ts
apps/frontend/**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

apps/frontend/**/*.{ts,tsx}: Review React patterns and TypeScript type safety.
Check for proper state management and component composition.

Files:

  • apps/frontend/src/main/ipc-handlers/env-handlers.ts
  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
  • apps/frontend/src/main/__tests__/env-utils.test.ts
  • apps/frontend/src/main/env-utils.ts
  • apps/frontend/src/main/cli-tool-manager.ts
  • apps/frontend/src/main/utils/windows-paths.ts
🧬 Code graph analysis (3)
apps/frontend/src/main/ipc-handlers/env-handlers.ts (1)
apps/frontend/src/main/env-utils.ts (1)
  • getSpawnOptions (454-475)
apps/frontend/src/main/__tests__/cli-tool-manager.test.ts (1)
apps/frontend/src/main/cli-tool-manager.ts (4)
  • sortNvmVersionDirs (205-226)
  • buildClaudeDetectionResult (249-265)
  • getToolPathAsync (986-1011)
  • getToolPathAsync (1829-1831)
apps/frontend/src/main/__tests__/env-utils.test.ts (1)
apps/frontend/src/main/env-utils.ts (2)
  • shouldUseShell (428-436)
  • getSpawnOptions (454-475)
🪛 GitHub Check: test-frontend
apps/frontend/src/main/__tests__/cli-tool-manager.test.ts

[warning] 10-10:
'app' is defined but never used. Allowed unused vars must match /^_/u


[warning] 9-9:
'execFile' is defined but never used. Allowed unused vars must match /^_/u


[warning] 68-68:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 202-202:
Unexpected any. Specify a different type


[warning] 137-137:
Unexpected any. Specify a different type

apps/frontend/src/main/cli-tool-manager.ts

[failure] 32-32: src/main/tests/cli-tool-manager.test.ts
Error: [vitest] No "exec" export is defined on the "child_process" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("child_process"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/main/cli-tool-manager.ts:32:29
❯ src/main/tests/cli-tool-manager.test.ts:11:1

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: CodeQL (python)
  • GitHub Check: CodeQL (javascript-typescript)
🔇 Additional comments (22)
apps/frontend/src/main/utils/windows-paths.ts (2)

158-163: LGTM! Extension preference logic is sound.

The logic to prefer .cmd or .exe extensions when where.exe returns multiple paths is correct. The fallback to paths[0] ensures robustness when no preferred extension is found, and the existing security validation provides additional safety.


261-266: LGTM! Async version mirrors sync logic correctly.

The async version applies the same extension preference logic as the sync version (lines 158-163), maintaining consistency across both code paths.

apps/frontend/src/main/ipc-handlers/env-handlers.ts (3)

606-609: LGTM! Spawn options refactored correctly.

The refactoring to use getSpawnOptions centralizes shell handling logic. The base options (cwd, env) are preserved and correctly passed through.


626-629: LGTM! Consistent with version check spawn.

Auth check spawn follows the same pattern as the version check (lines 606-609), ensuring consistent shell handling across all Claude CLI invocations.


695-699: LGTM! Setup flow uses getSpawnOptions correctly.

The setup-token spawn correctly includes stdio: 'inherit' in base options, which is essential for interactive OAuth flow. The getSpawnOptions wrapper preserves this configuration.

apps/frontend/src/main/__tests__/env-utils.test.ts (2)

1-78: LGTM! Comprehensive shouldUseShell test coverage.

The test suite thoroughly covers:

  • Windows: .cmd, .bat, .CMD, .BAT (true), .exe and extensionless (false)
  • Paths with spaces and special characters
  • Non-Windows platforms (macOS, Linux) always return false

The platform mocking approach using Object.defineProperty is correct and properly cleaned up in afterEach.


80-200: LGTM! getSpawnOptions tests cover all scenarios.

The tests validate:

  • Shell flag correctly set based on file extension (.cmd → true, .exe → false)
  • Base options preserved (cwd, env, timeout, windowsHide, stdio)
  • Empty base options handled gracefully
  • Non-Windows platforms set shell: false
  • Both .cmd and .bat files handled

Test coverage aligns well with the implementation.

apps/frontend/src/main/__tests__/cli-tool-manager.test.ts (6)

54-75: Mock implementation is promisify-compatible.

The execFile mock correctly implements the Node.js callback pattern expected by promisify(). The minimal ChildProcess-like object with event handlers ensures compatibility with async usage.

The any types on line 55 and 68 are acceptable in test mocks where full type safety is less critical. Consider adding a type comment if you want to suppress the linter warning:

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockExecFile = vi.fn((cmd: any, args: any, options: any, callback: any) => {

131-149: NVM test updated correctly for newest version selection.

The test now expects v22.17.0 (the newest version among the three) to be selected, which aligns with the sorting logic. The updated expectations for version and message are correct.


151-167: Test correctly validates NVM skipped on Windows.

The test verifies that readdirSync is not called on Windows, confirming NVM path detection is Unix-specific. The fallback source expectation is appropriate.


265-320: Platform-specific path tests provide good coverage.

The tests validate:

  • Windows AppData npm global path detection
  • Unix .local/bin path detection
  • Fallback behavior when Claude not found

These tests complement the NVM detection tests and cover the broader detection strategy.


480-664: Windows where.exe detection tests are comprehensive.

The test suite covers:

  • Successful detection via where.exe (nvm-windows location)
  • Platform-specific behavior (skip on non-Windows)
  • Validation before returning path
  • Fallback when where.exe fails
  • Extension preference (.cmd/.exe)
  • Error handling

These tests validate the fourth fallback detection step mentioned in the PR objectives.


666-727: Async where.exe tests validate non-blocking paths.

The async tests ensure findWindowsExecutableViaWhereAsync and getToolPathAsync work correctly without blocking the main process. The mock verification confirms where.exe is called with the correct arguments.

apps/frontend/src/main/env-utils.ts (2)

428-436: LGTM! shouldUseShell logic is correct and well-documented.

The function correctly identifies when shell execution is required:

  • Windows: returns true for .cmd and .bat files (case-insensitive)
  • Non-Windows: always returns false

The regex /\.(cmd|bat)$/i properly matches file extensions at the end of the path.


454-475: LGTM! getSpawnOptions provides clean abstraction.

The function correctly:

  • Spreads all base options (cwd, env, timeout, windowsHide, stdio)
  • Adds the shell flag computed by shouldUseShell
  • Returns a properly typed options object

This centralizes shell decision logic and eliminates the need for inline conditionals at spawn call sites.

apps/frontend/src/main/cli-tool-manager.ts (7)

42-49: LGTM!

The existsAsync helper correctly uses fs.promises.access for non-blocking file existence checks, which is the proper async alternative to existsSync.


164-186: LGTM!

Good refactoring to extract path configuration into a pure function. This centralizes platform-specific path logic and makes it testable independently.


205-226: LGTM!

Robust semver sorting with proper validation regex to filter out malformed directories. The descending sort ensures the newest Node.js version is checked first for Claude CLI.


249-265: LGTM!

Good helper function that reduces code duplication in Claude detection methods. Clear null-on-failure contract simplifies caller logic.


929-954: Correct approach for Windows .cmd/.bat execution with spaces.

The shell-aware execution strategy correctly handles the Windows limitation where execFileSync fails with .cmd/.bat files containing spaces. Using execSync with quoted paths is the appropriate workaround.

One consideration: if a path contained shell metacharacters beyond spaces (e.g., &, |, "), the simple quoting might not be sufficient. However, since paths come from known detection sources (not arbitrary user input), this is a low risk.


1046-1089: LGTM!

The async validation correctly mirrors the sync version with shell-aware execution, using the async variants (execAsync, execFileAsync, getAugmentedEnvAsync).


754-762: LGTM - Windows where.exe detection well-positioned.

The where.exe fallback is correctly placed after the system PATH check (step 4), providing reliable detection for non-standard Windows installations like nvm-windows. The async variant at lines 1265-1273 correctly uses findWindowsExecutableViaWhereAsync.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an excellent pull request that thoughtfully addresses a tricky Windows-specific issue with executing commands from paths containing spaces. The solution is robust, introducing a new execution strategy for .cmd and .bat files and improving detection logic with where.exe. The changes are well-supported by a comprehensive new suite of unit tests, which gives high confidence in the fix. The introduction of helper utilities like shouldUseShell and getSpawnOptions is a great step towards improving code clarity and maintainability. Overall, this is a high-quality contribution.

Comment on lines +57 to +77
describe('Non-Windows platforms', () => {
it('should return false on macOS', () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
writable: true,
configurable: true,
});
expect(shouldUseShell('/usr/local/bin/claude')).toBe(false);
expect(shouldUseShell('/opt/homebrew/bin/claude.cmd')).toBe(false);
});

it('should return false on Linux', () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
writable: true,
configurable: true,
});
expect(shouldUseShell('/usr/bin/claude')).toBe(false);
expect(shouldUseShell('/home/user/.local/bin/claude.bat')).toBe(false);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These two test cases for non-Windows platforms are very similar. You can make the test suite more concise and easier to extend by combining them into a single, parameterized test using it.each from Vitest.

  describe('Non-Windows platforms', () => {
    it.each([
      ['darwin', '/usr/local/bin/claude'],
      ['darwin', '/opt/homebrew/bin/claude.cmd'],
      ['linux', '/usr/bin/claude'],
      ['linux', '/home/user/.local/bin/claude.bat'],
    ])('should return false on %s for path %s', (platform, path) => {
      Object.defineProperty(process, 'platform', {
        value: platform,
        writable: true,
        configurable: true,
      });
      expect(shouldUseShell(path)).toBe(false);
    });
  });

Comment on lines +939 to +953
version = execSync(`${quotedCmd} --version`, {
encoding: 'utf-8',
timeout: 5000,
windowsHide: true,
env: getAugmentedEnv(),
}).trim();
} else {
// For .exe files and non-Windows, use execFileSync
version = execFileSync(claudeCmd, ['--version'], {
encoding: 'utf-8',
timeout: 5000,
windowsHide: true,
shell: false,
env: getAugmentedEnv(),
}).trim();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The options for execSync and execFileSync are very similar. To reduce duplication and improve maintainability, you could define a constant for the common options (encoding, timeout, windowsHide) and spread it into the options objects. This would also apply to the similar logic in validateClaudeAsync.

@AndyMik90 AndyMik90 self-assigned this Jan 8, 2026
Copy link
Owner

@AndyMik90 AndyMik90 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Claude PR Review

Merge Verdict: 🔴 BLOCKED

🔴 Blocked - 1 CI check(s) failing. Fix CI before merge.

Blocked: 1 CI check(s) failing. Fix CI before merge.

Risk Assessment

Factor Level Notes
Complexity High Based on lines changed
Security Impact Medium Based on security findings
Scope Coherence Good Based on structural review

🚨 Blocking Issues (Must Fix)

  • Branch Out of Date: PR branch is behind the base branch and needs to be updated
  • CI Failed: test-frontend

Findings Summary

  • High: 2 issue(s)
  • Low: 2 issue(s)

Generated by Auto Claude PR Review

Findings (4 selected of 4 total)

🟠 [2c259fa6304b] [HIGH] Command injection risk via quoted path in validateClaude

📁 apps/frontend/src/main/cli-tool-manager.ts:938

The validateClaude() method constructs a shell command using simple double quotes: const quotedCmd = \"${claudeCmd}"`; then executes execSync(`${quotedCmd} --version`). If claudeCmd contains a double quote character (e.g., from user configuration via settings.claudePath), this could break out of the quoting and enable command injection. Example: a path like C:\Program Files"&calc&"\claude.cmdwould result in"C:\Program Files"&calc&"\claude.cmd" --version, executing calc`. While practical exploitability is LOW (attacker needs to convince user to configure malicious path, path must exist on disk), this is the root cause of the GitHub Advanced Security 'Indirect uncontrolled command line' alerts.

Suggested fix:

Instead of using execSync with shell quoting, use execFileSync with cmd.exe: `execFileSync('cmd.exe', ['/c', claudeCmd, '--version'], {windowsHide: true, timeout: 5000, env: getAugmentedEnv()})`. This prevents argument injection by separating the command from its arguments. Alternatively, validate that claudeCmd does not contain shell metacharacters (&, |, ", etc.) before execution.

🟠 [fc24fd507376] [HIGH] Same command injection risk in validateClaudeAsync

📁 apps/frontend/src/main/cli-tool-manager.ts:1054

The async version validateClaudeAsync() has the identical vulnerability pattern - it uses execAsync (promisified exec) with a quoted command path for .cmd/.bat files. The same attack vector applies: a user-configured claudePath containing shell metacharacters could potentially escape the quoting. This is the second GitHub Advanced Security alert.

Suggested fix:

Use execFileAsync with cmd.exe instead: `await execFileAsync('cmd.exe', ['/c', claudeCmd, '--version'], {windowsHide: true, timeout: 5000, env: await getAugmentedEnvAsync()})`. This properly separates the command from arguments.

🔵 [93d4926a44cf] [LOW] Extension preference may select less secure executable type

📁 apps/frontend/src/main/utils/windows-paths.ts:163

When where.exe returns multiple paths, the code uses paths.find(p => /\.(cmd|exe)$/i.test(p)) || paths[0] which prefers .cmd or .exe equally (first match wins). If both claude.cmd and claude.exe exist, .cmd may be selected first, requiring shell execution with the quoting vulnerability. Preferring .exe over .cmd would be more secure since .exe files don't require shell execution.

Suggested fix:

Prioritize .exe over .cmd: `paths.find(p => /\.exe$/i.test(p)) || paths.find(p => /\.cmd$/i.test(p)) || paths[0]`

🔵 [bc273cf8287c] [LOW] Documentation: duplicate step numbers in detection comments

📁 apps/frontend/src/main/cli-tool-manager.ts:785

The detection code has two steps labeled '5' - NVM paths and platform-specific locations. The docstring at line 706-711 also shows inconsistent numbering with the implementation. This is a documentation issue that could cause confusion during maintenance.

Suggested fix:

Renumber comments: step 4 = where.exe, step 5 = NVM paths, step 6 = platform paths, step 7 = not found. Update both sync and async versions, plus the docstring.

This review was generated by Auto Claude.

g1331 added 4 commits January 9, 2026 11:34
- Add where.exe as fallback detection method on Windows (step 4)
- Enables detection of Claude CLI in non-standard paths (e.g., nvm-windows)
- where.exe searches PATH + Windows Registry + current directory
- Add 8 comprehensive unit tests (6 sync + 2 async)
- Update JSDoc comments to reflect new detection priority

Fixes issue where Claude CLI installed via nvm-windows or other
non-standard locations cannot be detected by standard PATH search.
The where.exe utility is universally available on Windows and provides
more comprehensive executable resolution than basic PATH checks.

Signed-off-by: yc13 <[email protected]>
When where.exe finds multiple paths for the same executable (e.g., both
'claude' and 'claude.cmd'), we now prefer paths with .cmd or .exe extensions
since Windows requires extensions to execute files.

This fixes Claude CLI detection for nvm-windows installations where the
executable is installed as claude.cmd but where.exe returns the extensionless
path first.

Signed-off-by: yc13 <[email protected]>
…Windows

Root cause: execFileSync cannot handle paths with spaces in .cmd/.bat files,
even with shell:true. Windows requires shell to execute batch files.

Solution:
- Add shouldUseShell() utility to detect .cmd/.bat files
- Use execSync (not execFileSync) for .cmd/.bat files with quoted paths
- Use getSpawnOptions() for spawn() calls in env-handlers.ts
- Add comprehensive unit tests (15 test cases)

Technical details:
- execFileSync + shell:true: FAILS with space in path
- execSync with quoted path: WORKS correctly
- spawn with getSpawnOptions(): WORKS correctly

Files changed:
- env-utils.ts: Add shouldUseShell() and getSpawnOptions()
- cli-tool-manager.ts: Use execSync/execAsync for .cmd/.bat validation
- env-handlers.ts: Use getSpawnOptions() for spawn calls
- env-utils.test.ts: Add 15 unit tests

Fixes issue where Claude CLI in paths like 'D:\Program Files\nvm4w\nodejs\claude.cmd'
fails with error: 'D:\Program' is not recognized as internal or external command.

Signed-off-by: g1331 <[email protected]>
…nt numbering

- Remove unused imports (execFile, app, execSync, mockDirent)
- Fix duplicate step numbering in Claude CLI detection comments (5→6, 6→7)
- Add exec mock to child_process for async validation support
- Add shouldUseShell and getSpawnOptions mocks for Windows .cmd handling
@g1331 g1331 force-pushed the fix/claude-cli-detection-windows-where branch from 8f25c29 to e2c5978 Compare January 9, 2026 03:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/frontend/src/main/utils/windows-paths.ts (1)

157-172: Include .bat in executable preference and iterate candidates by priority order.

Current code omits .bat from the preferred extension list despite treating it identically to .cmd throughout the codebase for shell execution requirements. Additionally, the logic picks one candidate first, then validates—it's safer to validate all candidates in preference order and return the first match.

Suggested fix
-    // 'where' returns multiple paths separated by newlines if found in multiple locations
-    // Prefer paths with .cmd or .exe extensions (executable files)
+    // 'where' returns multiple paths separated by newlines if found in multiple locations
+    // Prefer executable/script extensions (Windows)
     const paths = result.split(/\r?\n/).filter(p => p.trim());
 
     if (paths.length > 0) {
-      // Prefer .cmd or .exe extensions, otherwise take first path
-      const foundPath = (paths.find(p => /\.(cmd|exe)$/i.test(p)) || paths[0]).trim();
-
-      // Validate the path exists and is secure
-      if (existsSync(foundPath) && isSecurePath(foundPath)) {
-        console.log(`${logPrefix} Found via where: ${foundPath}`);
-        return foundPath;
-      }
+      const preferred = paths.filter(p => /\.(cmd|bat|exe)$/i.test(p));
+      const candidates = [...preferred, ...paths].map(p => p.trim());
+      const foundPath = candidates.find(p => existsSync(p) && isSecurePath(p));
+      if (foundPath) {
+        console.log(`${logPrefix} Found via where: ${foundPath}`);
+        return foundPath;
+      }
     }
🤖 Fix all issues with AI agents
In @apps/frontend/src/main/__tests__/cli-tool-manager.test.ts:
- Around line 50-93: The child_process mock uses broad any types which reduces
type safety; update the mock definitions (mockExecFile, mockExec, and the vi.fn
signatures used in vi.mock) to use explicit parameter and callback types (e.g.,
cmd: string, args: string[], options: Record<string, any> or SpawnOptions, and
callback: (error: Error | null, stdout: string, stderr: string) => void) so
TypeScript can catch mismatches while keeping the same mock behavior; keep
sharedSyncMock as a vi.fn() for execFileSync/execSync but give it an appropriate
typed signature like (...args: any[]) => Buffer | string to reflect
execFileSync/execSync return types.

In @apps/frontend/src/main/__tests__/env-handlers-claude-cli.test.ts:
- Around line 44-66: The mockExecFile in the vi.mock block is too rigid for
Node's execFile overloads and uses setImmediate which can flake tests; update
mockExecFile to detect the actual call signature by checking types and locating
the callback as the last function argument (supporting (cmd), (cmd, cb), (cmd,
args, cb), (cmd, args, options, cb)), normalize args/options to sensible
defaults, and invoke the callback via process.nextTick (or synchronously)
instead of setImmediate; keep the returned stubbed ChildProcess object and
ensure spawnMock, execFileSync and execFile remain exported.

In @apps/frontend/src/main/__tests__/env-utils.test.ts:
- Around line 1-200: The tests currently overwrite process.platform using a
saved primitive originalPlatform and restore it by redefining platform as
writable/configurable, which leaves the platform property mutable; instead
capture the original property descriptor with
Object.getOwnPropertyDescriptor(process, 'platform') at the top of each describe
block and in afterEach restore that descriptor via
Object.defineProperty(process, 'platform', originalDescriptor). Update both
describe blocks around shouldUseShell and getSpawnOptions to use
originalDescriptor (not originalPlatform) and ensure every beforeEach still uses
Object.defineProperty to set the mocked platform string so tests for
shouldUseShell and getSpawnOptions behave the same while fully restoring the
original descriptor afterwards.

In @apps/frontend/src/main/cli-tool-manager.ts:
- Around line 931-954: The code interpolates user-controlled claudeCmd into an
execSync shell string in validateClaude() and validateClaudeAsync(), allowing
shell injection; fix by never building a shell command string with the user path
— use execFileSync(claudeCmd, ['--version'], {...}) for all cases (or
execFileSync with shell:true if Windows requires cmd processing) so arguments
are passed separately, and add a defensive validation on claudeCmd in both
validateClaude and validateClaudeAsync (reject quotes, newlines and shell
metacharacters like ;&|`$(){}[]<>!^ or use a proven escaping library such as
shell-escape) before executing.

In @apps/frontend/src/main/ipc-handlers/env-handlers.ts:
- Around line 13-14: The spawn call-sites are failing on Windows when shell:
true because unquoted command paths with spaces (e.g., claudeCmd) break; update
each place that calls spawn(...) with a command variable like claudeCmd (and
other CLI command vars referenced near the noted ranges) to wrap the command
path in double quotes when running on Windows and shell mode is enabled (mirror
the quoting logic used in cli-tool-manager.ts validation), e.g., build the spawn
command as `"${claudeCmd}"` before passing to spawn, ensuring getSpawnOptions or
platform detection is respected so non-Windows platforms are unchanged.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f25c29 and e2c5978.

📒 Files selected for processing (7)
  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
  • apps/frontend/src/main/__tests__/env-handlers-claude-cli.test.ts
  • apps/frontend/src/main/__tests__/env-utils.test.ts
  • apps/frontend/src/main/cli-tool-manager.ts
  • apps/frontend/src/main/env-utils.ts
  • apps/frontend/src/main/ipc-handlers/env-handlers.ts
  • apps/frontend/src/main/utils/windows-paths.ts
🧰 Additional context used
📓 Path-based instructions (3)
apps/frontend/src/**/*.{ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use i18n translation keys for all user-facing text in the frontend instead of hardcoded strings

Files:

  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
  • apps/frontend/src/main/__tests__/env-utils.test.ts
  • apps/frontend/src/main/ipc-handlers/env-handlers.ts
  • apps/frontend/src/main/cli-tool-manager.ts
  • apps/frontend/src/main/utils/windows-paths.ts
  • apps/frontend/src/main/__tests__/env-handlers-claude-cli.test.ts
  • apps/frontend/src/main/env-utils.ts
apps/frontend/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use useTranslation() hook with namespace prefixes (e.g., 'navigation:items.key') for accessing translation strings in React components

Files:

  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
  • apps/frontend/src/main/__tests__/env-utils.test.ts
  • apps/frontend/src/main/ipc-handlers/env-handlers.ts
  • apps/frontend/src/main/cli-tool-manager.ts
  • apps/frontend/src/main/utils/windows-paths.ts
  • apps/frontend/src/main/__tests__/env-handlers-claude-cli.test.ts
  • apps/frontend/src/main/env-utils.ts
apps/frontend/**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

apps/frontend/**/*.{ts,tsx}: Review React patterns and TypeScript type safety.
Check for proper state management and component composition.

Files:

  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
  • apps/frontend/src/main/__tests__/env-utils.test.ts
  • apps/frontend/src/main/ipc-handlers/env-handlers.ts
  • apps/frontend/src/main/cli-tool-manager.ts
  • apps/frontend/src/main/utils/windows-paths.ts
  • apps/frontend/src/main/__tests__/env-handlers-claude-cli.test.ts
  • apps/frontend/src/main/env-utils.ts
🧬 Code graph analysis (4)
apps/frontend/src/main/__tests__/cli-tool-manager.test.ts (3)
apps/frontend/src/main/cli-tool-manager.ts (8)
  • getToolInfo (1691-1693)
  • getToolInfo (1767-1769)
  • getClaudeDetectionPaths (164-186)
  • sortNvmVersionDirs (205-226)
  • buildClaudeDetectionResult (249-265)
  • clearToolCache (1785-1787)
  • getToolPathAsync (986-1011)
  • getToolPathAsync (1829-1831)
apps/frontend/src/main/env-utils.ts (2)
  • findExecutable (252-273)
  • findExecutableAsync (411-430)
apps/frontend/src/main/utils/windows-paths.ts (2)
  • findWindowsExecutableViaWhere (134-177)
  • findWindowsExecutableViaWhereAsync (237-285)
apps/frontend/src/main/__tests__/env-utils.test.ts (1)
apps/frontend/src/main/env-utils.ts (2)
  • shouldUseShell (460-468)
  • getSpawnOptions (486-507)
apps/frontend/src/main/ipc-handlers/env-handlers.ts (1)
apps/frontend/src/main/env-utils.ts (1)
  • getSpawnOptions (486-507)
apps/frontend/src/main/__tests__/env-handlers-claude-cli.test.ts (1)
apps/frontend/scripts/download-prebuilds.cjs (1)
  • options (59-66)
🪛 GitHub Actions: CI
apps/frontend/src/main/__tests__/cli-tool-manager.test.ts

[error] 322-322: Platform-specific path detection should detect Claude CLI in Windows AppData npm global path. AssertionError: expected false to be true.

🪛 GitHub Check: test-frontend
apps/frontend/src/main/__tests__/cli-tool-manager.test.ts

[warning] 108-108:
Unexpected any. Specify a different type


[warning] 84-84:
Unexpected any. Specify a different type


[warning] 71-71:
Unexpected any. Specify a different type


[warning] 71-71:
Unexpected any. Specify a different type


[warning] 71-71:
Unexpected any. Specify a different type


[warning] 68-68:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[failure] 322-322: src/main/tests/cli-tool-manager.test.ts > cli-tool-manager - Claude CLI NVM detection > Platform-specific path detection > should detect Claude CLI in Windows AppData npm global path
AssertionError: expected false to be true // Object.is equality

  • Expected
  • Received
  • true
  • false

❯ src/main/tests/cli-tool-manager.test.ts:322:28

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: CodeQL (javascript-typescript)
  • GitHub Check: CodeQL (python)
🔇 Additional comments (8)
apps/frontend/src/main/cli-tool-manager.ts (2)

23-33: OK direction: async + shell-aware execution primitives.
Imports/async wrappers align with the PR goals.


754-785: Windows where.exe detection stage for Claude is a solid fallback.
Ordering and win32-guarding look right.

Also applies to: 1265-1276

apps/frontend/src/main/utils/windows-paths.ts (1)

260-281: File not found in repository: The file apps/frontend/src/main/utils/windows-paths.ts referenced in this review comment does not exist in the current repository state. Manual verification is required to confirm whether this file path is correct and to validate the proposed refactoring approach.

apps/frontend/src/main/env-utils.ts (2)

442-468: LGTM! Well-designed utility for Windows shell detection.

The implementation correctly identifies when shell execution is required for Windows .cmd/.bat files. The regex pattern is properly anchored and case-insensitive, and the documentation with examples is excellent.


486-507: LGTM! Clean abstraction for spawn options.

The function provides a consistent interface for setting up spawn options with platform-appropriate shell settings. The implementation correctly merges base options and overrides the shell field based on the command type.

apps/frontend/src/main/__tests__/cli-tool-manager.test.ts (3)

524-682: Comprehensive Windows where.exe test coverage.

The test suite for Windows where.exe detection is well-structured with good coverage of:

  • Success path with nvm-windows locations
  • Platform-specific skipping
  • Validation requirements
  • Fallback mechanisms
  • Extension preferences
  • Error handling

687-730: Good async test coverage for where.exe detection.

The async tests properly verify that findWindowsExecutableViaWhereAsync is called and errors are handled gracefully. The tests use async/await correctly and validate the fallback behavior.


302-325: The test mock setup appears complete and correct. Global mocks properly configure findExecutable() and findWindowsExecutableViaWhere() to return null, forcing the detection to reach the platformPaths checking stage. The test mocks existsSync to return true for the AppData path, and execFileSync/execSync (which share the same mock) to return valid version output. The detection logic correctly:

  1. Skips user config and homebrew (darwin-only) for win32 platform
  2. Returns null from the mocked findExecutable and findWindowsExecutableViaWhere calls
  3. Skips NVM paths (Unix-only for non-win32)
  4. Iterates platformPaths which includes the AppData location
  5. Validates the path via execSync (which uses the shared mock)
  6. Constructs a successful detection result

The code and test setup appear functionally correct. Without access to actual test execution results, I cannot confirm whether this test currently passes or fails. If the test is actually failing, the issue may require runtime diagnostics beyond code inspection.

Comment on lines +44 to +66
vi.mock('child_process', () => {
const mockExecFile = vi.fn((cmd: any, args: any, options: any, callback: any) => {
// Return a minimal ChildProcess-like object
const childProcess = {
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn()
};

// If callback is provided, call it asynchronously
if (typeof callback === 'function') {
setImmediate(() => callback(null, '', ''));
}

return childProcess as any;
});

return {
spawn: spawnMock,
execFileSync: vi.fn(),
execFile: mockExecFile
};
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Harden execFile mock to support Node overloads + reduce timing flakiness.
Right now it assumes (cmd, args, options, callback); if a future code path uses (cmd, args, cb) the mock will mis-handle args/options. Also setImmediate can be awkward with fake timers.

Proposed diff
 vi.mock('child_process', () => {
-  const mockExecFile = vi.fn((cmd: any, args: any, options: any, callback: any) => {
+  const mockExecFile = vi.fn((cmd: any, args: any, options: any, callback: any) => {
+    // Support overloads:
+    // - (cmd, args, cb)
+    // - (cmd, cb)
+    let cb = callback;
+    if (typeof options === 'function') {
+      cb = options;
+    } else if (typeof args === 'function') {
+      cb = args;
+    }
+
     // Return a minimal ChildProcess-like object
     const childProcess = {
       stdout: { on: vi.fn() },
       stderr: { on: vi.fn() },
       on: vi.fn()
     };
 
     // If callback is provided, call it asynchronously
-    if (typeof callback === 'function') {
-      setImmediate(() => callback(null, '', ''));
+    if (typeof cb === 'function') {
+      queueMicrotask(() => cb(null, '', ''));
     }
 
     return childProcess as any;
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vi.mock('child_process', () => {
const mockExecFile = vi.fn((cmd: any, args: any, options: any, callback: any) => {
// Return a minimal ChildProcess-like object
const childProcess = {
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn()
};
// If callback is provided, call it asynchronously
if (typeof callback === 'function') {
setImmediate(() => callback(null, '', ''));
}
return childProcess as any;
});
return {
spawn: spawnMock,
execFileSync: vi.fn(),
execFile: mockExecFile
};
});
vi.mock('child_process', () => {
const mockExecFile = vi.fn((cmd: any, args: any, options: any, callback: any) => {
// Support overloads:
// - (cmd, args, cb)
// - (cmd, cb)
let cb = callback;
if (typeof options === 'function') {
cb = options;
} else if (typeof args === 'function') {
cb = args;
}
// Return a minimal ChildProcess-like object
const childProcess = {
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn()
};
// If callback is provided, call it asynchronously
if (typeof cb === 'function') {
queueMicrotask(() => cb(null, '', ''));
}
return childProcess as any;
});
return {
spawn: spawnMock,
execFileSync: vi.fn(),
execFile: mockExecFile
};
});
🤖 Prompt for AI Agents
In @apps/frontend/src/main/__tests__/env-handlers-claude-cli.test.ts around
lines 44 - 66, The mockExecFile in the vi.mock block is too rigid for Node's
execFile overloads and uses setImmediate which can flake tests; update
mockExecFile to detect the actual call signature by checking types and locating
the callback as the last function argument (supporting (cmd), (cmd, cb), (cmd,
args, cb), (cmd, args, options, cb)), normalize args/options to sensible
defaults, and invoke the callback via process.nextTick (or synchronously)
instead of setImmediate; keep the returned stubbed ChildProcess object and
ensure spawnMock, execFileSync and execFile remain exported.

Comment on lines +1 to +200
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { shouldUseShell, getSpawnOptions } from '../env-utils';

describe('shouldUseShell', () => {
const originalPlatform = process.platform;

afterEach(() => {
// Restore original platform after each test
Object.defineProperty(process, 'platform', {
value: originalPlatform,
writable: true,
configurable: true,
});
});

describe('Windows platform', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
});

it('should return true for .cmd files', () => {
expect(shouldUseShell('D:\\Program Files\\nodejs\\claude.cmd')).toBe(true);
expect(shouldUseShell('C:\\Users\\admin\\AppData\\Roaming\\npm\\claude.cmd')).toBe(true);
});

it('should return true for .bat files', () => {
expect(shouldUseShell('C:\\batch\\script.bat')).toBe(true);
});

it('should return true for .CMD (uppercase)', () => {
expect(shouldUseShell('D:\\Tools\\CLAUDE.CMD')).toBe(true);
});

it('should return true for .BAT (uppercase)', () => {
expect(shouldUseShell('C:\\Scripts\\SETUP.BAT')).toBe(true);
});

it('should return false for .exe files', () => {
expect(shouldUseShell('C:\\Windows\\System32\\git.exe')).toBe(false);
});

it('should return false for extensionless files', () => {
expect(shouldUseShell('D:\\Git\\bin\\bash')).toBe(false);
});

it('should handle paths with spaces and special characters', () => {
expect(shouldUseShell('D:\\Program Files (x86)\\tool.cmd')).toBe(true);
expect(shouldUseShell('D:\\Path&Name\\tool.cmd')).toBe(true);
expect(shouldUseShell('D:\\Program Files (x86)\\tool.exe')).toBe(false);
});
});

describe('Non-Windows platforms', () => {
it('should return false on macOS', () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
writable: true,
configurable: true,
});
expect(shouldUseShell('/usr/local/bin/claude')).toBe(false);
expect(shouldUseShell('/opt/homebrew/bin/claude.cmd')).toBe(false);
});

it('should return false on Linux', () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
writable: true,
configurable: true,
});
expect(shouldUseShell('/usr/bin/claude')).toBe(false);
expect(shouldUseShell('/home/user/.local/bin/claude.bat')).toBe(false);
});
});
});

describe('getSpawnOptions', () => {
const originalPlatform = process.platform;

afterEach(() => {
// Restore original platform after each test
Object.defineProperty(process, 'platform', {
value: originalPlatform,
writable: true,
configurable: true,
});
});

it('should set shell: true for .cmd files on Windows', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});

const opts = getSpawnOptions('D:\\nodejs\\claude.cmd', {
cwd: 'D:\\project',
env: { PATH: 'C:\\Windows' },
});

expect(opts).toEqual({
cwd: 'D:\\project',
env: { PATH: 'C:\\Windows' },
shell: true,
});
});

it('should set shell: false for .exe files on Windows', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});

const opts = getSpawnOptions('C:\\Windows\\git.exe', {
cwd: 'D:\\project',
});

expect(opts).toEqual({
cwd: 'D:\\project',
shell: false,
});
});

it('should preserve all base options including stdio', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});

const opts = getSpawnOptions('D:\\tool.cmd', {
cwd: 'D:\\project',
env: { FOO: 'bar' },
timeout: 5000,
windowsHide: true,
stdio: 'inherit',
});

expect(opts).toEqual({
cwd: 'D:\\project',
env: { FOO: 'bar' },
timeout: 5000,
windowsHide: true,
stdio: 'inherit',
shell: true,
});
});

it('should handle empty base options', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});

const opts = getSpawnOptions('D:\\tool.cmd');

expect(opts).toEqual({
shell: true,
});
});

it('should set shell: false on non-Windows platforms', () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
writable: true,
configurable: true,
});

const opts = getSpawnOptions('/usr/local/bin/claude', {
cwd: '/project',
});

expect(opts).toEqual({
cwd: '/project',
shell: false,
});
});

it('should handle .bat files on Windows', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});

const opts = getSpawnOptions('C:\\scripts\\setup.bat', {
cwd: 'D:\\project',
});

expect(opts).toEqual({
cwd: 'D:\\project',
shell: true,
});
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Nice coverage; make platform mocking more robust by restoring the original descriptor.
This avoids leaving process.platform writable/configurable for later tests.

Proposed diff
 describe('shouldUseShell', () => {
-  const originalPlatform = process.platform;
+  const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
+  const originalPlatform = process.platform;
 
   afterEach(() => {
     // Restore original platform after each test
-    Object.defineProperty(process, 'platform', {
-      value: originalPlatform,
-      writable: true,
-      configurable: true,
-    });
+    if (originalPlatformDescriptor) {
+      Object.defineProperty(process, 'platform', originalPlatformDescriptor);
+    } else {
+      Object.defineProperty(process, 'platform', { value: originalPlatform });
+    }
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { shouldUseShell, getSpawnOptions } from '../env-utils';
describe('shouldUseShell', () => {
const originalPlatform = process.platform;
afterEach(() => {
// Restore original platform after each test
Object.defineProperty(process, 'platform', {
value: originalPlatform,
writable: true,
configurable: true,
});
});
describe('Windows platform', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
});
it('should return true for .cmd files', () => {
expect(shouldUseShell('D:\\Program Files\\nodejs\\claude.cmd')).toBe(true);
expect(shouldUseShell('C:\\Users\\admin\\AppData\\Roaming\\npm\\claude.cmd')).toBe(true);
});
it('should return true for .bat files', () => {
expect(shouldUseShell('C:\\batch\\script.bat')).toBe(true);
});
it('should return true for .CMD (uppercase)', () => {
expect(shouldUseShell('D:\\Tools\\CLAUDE.CMD')).toBe(true);
});
it('should return true for .BAT (uppercase)', () => {
expect(shouldUseShell('C:\\Scripts\\SETUP.BAT')).toBe(true);
});
it('should return false for .exe files', () => {
expect(shouldUseShell('C:\\Windows\\System32\\git.exe')).toBe(false);
});
it('should return false for extensionless files', () => {
expect(shouldUseShell('D:\\Git\\bin\\bash')).toBe(false);
});
it('should handle paths with spaces and special characters', () => {
expect(shouldUseShell('D:\\Program Files (x86)\\tool.cmd')).toBe(true);
expect(shouldUseShell('D:\\Path&Name\\tool.cmd')).toBe(true);
expect(shouldUseShell('D:\\Program Files (x86)\\tool.exe')).toBe(false);
});
});
describe('Non-Windows platforms', () => {
it('should return false on macOS', () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
writable: true,
configurable: true,
});
expect(shouldUseShell('/usr/local/bin/claude')).toBe(false);
expect(shouldUseShell('/opt/homebrew/bin/claude.cmd')).toBe(false);
});
it('should return false on Linux', () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
writable: true,
configurable: true,
});
expect(shouldUseShell('/usr/bin/claude')).toBe(false);
expect(shouldUseShell('/home/user/.local/bin/claude.bat')).toBe(false);
});
});
});
describe('getSpawnOptions', () => {
const originalPlatform = process.platform;
afterEach(() => {
// Restore original platform after each test
Object.defineProperty(process, 'platform', {
value: originalPlatform,
writable: true,
configurable: true,
});
});
it('should set shell: true for .cmd files on Windows', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('D:\\nodejs\\claude.cmd', {
cwd: 'D:\\project',
env: { PATH: 'C:\\Windows' },
});
expect(opts).toEqual({
cwd: 'D:\\project',
env: { PATH: 'C:\\Windows' },
shell: true,
});
});
it('should set shell: false for .exe files on Windows', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('C:\\Windows\\git.exe', {
cwd: 'D:\\project',
});
expect(opts).toEqual({
cwd: 'D:\\project',
shell: false,
});
});
it('should preserve all base options including stdio', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('D:\\tool.cmd', {
cwd: 'D:\\project',
env: { FOO: 'bar' },
timeout: 5000,
windowsHide: true,
stdio: 'inherit',
});
expect(opts).toEqual({
cwd: 'D:\\project',
env: { FOO: 'bar' },
timeout: 5000,
windowsHide: true,
stdio: 'inherit',
shell: true,
});
});
it('should handle empty base options', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('D:\\tool.cmd');
expect(opts).toEqual({
shell: true,
});
});
it('should set shell: false on non-Windows platforms', () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('/usr/local/bin/claude', {
cwd: '/project',
});
expect(opts).toEqual({
cwd: '/project',
shell: false,
});
});
it('should handle .bat files on Windows', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('C:\\scripts\\setup.bat', {
cwd: 'D:\\project',
});
expect(opts).toEqual({
cwd: 'D:\\project',
shell: true,
});
});
});
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { shouldUseShell, getSpawnOptions } from '../env-utils';
describe('shouldUseShell', () => {
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
const originalPlatform = process.platform;
afterEach(() => {
// Restore original platform after each test
if (originalPlatformDescriptor) {
Object.defineProperty(process, 'platform', originalPlatformDescriptor);
} else {
Object.defineProperty(process, 'platform', { value: originalPlatform });
}
});
describe('Windows platform', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
});
it('should return true for .cmd files', () => {
expect(shouldUseShell('D:\\Program Files\\nodejs\\claude.cmd')).toBe(true);
expect(shouldUseShell('C:\\Users\\admin\\AppData\\Roaming\\npm\\claude.cmd')).toBe(true);
});
it('should return true for .bat files', () => {
expect(shouldUseShell('C:\\batch\\script.bat')).toBe(true);
});
it('should return true for .CMD (uppercase)', () => {
expect(shouldUseShell('D:\\Tools\\CLAUDE.CMD')).toBe(true);
});
it('should return true for .BAT (uppercase)', () => {
expect(shouldUseShell('C:\\Scripts\\SETUP.BAT')).toBe(true);
});
it('should return false for .exe files', () => {
expect(shouldUseShell('C:\\Windows\\System32\\git.exe')).toBe(false);
});
it('should return false for extensionless files', () => {
expect(shouldUseShell('D:\\Git\\bin\\bash')).toBe(false);
});
it('should handle paths with spaces and special characters', () => {
expect(shouldUseShell('D:\\Program Files (x86)\\tool.cmd')).toBe(true);
expect(shouldUseShell('D:\\Path&Name\\tool.cmd')).toBe(true);
expect(shouldUseShell('D:\\Program Files (x86)\\tool.exe')).toBe(false);
});
});
describe('Non-Windows platforms', () => {
it('should return false on macOS', () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
writable: true,
configurable: true,
});
expect(shouldUseShell('/usr/local/bin/claude')).toBe(false);
expect(shouldUseShell('/opt/homebrew/bin/claude.cmd')).toBe(false);
});
it('should return false on Linux', () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
writable: true,
configurable: true,
});
expect(shouldUseShell('/usr/bin/claude')).toBe(false);
expect(shouldUseShell('/home/user/.local/bin/claude.bat')).toBe(false);
});
});
});
describe('getSpawnOptions', () => {
const originalPlatform = process.platform;
afterEach(() => {
// Restore original platform after each test
Object.defineProperty(process, 'platform', {
value: originalPlatform,
writable: true,
configurable: true,
});
});
it('should set shell: true for .cmd files on Windows', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('D:\\nodejs\\claude.cmd', {
cwd: 'D:\\project',
env: { PATH: 'C:\\Windows' },
});
expect(opts).toEqual({
cwd: 'D:\\project',
env: { PATH: 'C:\\Windows' },
shell: true,
});
});
it('should set shell: false for .exe files on Windows', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('C:\\Windows\\git.exe', {
cwd: 'D:\\project',
});
expect(opts).toEqual({
cwd: 'D:\\project',
shell: false,
});
});
it('should preserve all base options including stdio', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('D:\\tool.cmd', {
cwd: 'D:\\project',
env: { FOO: 'bar' },
timeout: 5000,
windowsHide: true,
stdio: 'inherit',
});
expect(opts).toEqual({
cwd: 'D:\\project',
env: { FOO: 'bar' },
timeout: 5000,
windowsHide: true,
stdio: 'inherit',
shell: true,
});
});
it('should handle empty base options', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('D:\\tool.cmd');
expect(opts).toEqual({
shell: true,
});
});
it('should set shell: false on non-Windows platforms', () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('/usr/local/bin/claude', {
cwd: '/project',
});
expect(opts).toEqual({
cwd: '/project',
shell: false,
});
});
it('should handle .bat files on Windows', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
writable: true,
configurable: true,
});
const opts = getSpawnOptions('C:\\scripts\\setup.bat', {
cwd: 'D:\\project',
});
expect(opts).toEqual({
cwd: 'D:\\project',
shell: true,
});
});
});
🤖 Prompt for AI Agents
In @apps/frontend/src/main/__tests__/env-utils.test.ts around lines 1 - 200, The
tests currently overwrite process.platform using a saved primitive
originalPlatform and restore it by redefining platform as writable/configurable,
which leaves the platform property mutable; instead capture the original
property descriptor with Object.getOwnPropertyDescriptor(process, 'platform') at
the top of each describe block and in afterEach restore that descriptor via
Object.defineProperty(process, 'platform', originalDescriptor). Update both
describe blocks around shouldUseShell and getSpawnOptions to use
originalDescriptor (not originalPlatform) and ensure every beforeEach still uses
Object.defineProperty to set the mocked platform string so tests for
shouldUseShell and getSpawnOptions behave the same while fully restoring the
original descriptor afterwards.

Comment on lines +931 to +954
const needsShell = shouldUseShell(claudeCmd);

let version: string;

if (needsShell) {
// For .cmd/.bat files on Windows, use execSync with quoted path
// execFileSync doesn't handle spaces in .cmd paths correctly even with shell:true
const quotedCmd = `"${claudeCmd}"`;
version = execSync(`${quotedCmd} --version`, {
encoding: 'utf-8',
timeout: 5000,
windowsHide: true,
env: getAugmentedEnv(),
}).trim();
} else {
// For .exe files and non-Windows, use execFileSync
version = execFileSync(claudeCmd, ['--version'], {
encoding: 'utf-8',
timeout: 5000,
windowsHide: true,
shell: false,
env: getAugmentedEnv(),
}).trim();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "cli-tool-manager.ts" -o -name "cli-tool-manager.js"

Repository: AndyMik90/Auto-Claude

Length of output: 109


🏁 Script executed:

wc -l apps/frontend/src/main/cli-tool-manager.ts

Repository: AndyMik90/Auto-Claude

Length of output: 112


🏁 Script executed:

sed -n '925,960p' apps/frontend/src/main/cli-tool-manager.ts

Repository: AndyMik90/Auto-Claude

Length of output: 1290


🏁 Script executed:

sed -n '1040,1080p' apps/frontend/src/main/cli-tool-manager.ts

Repository: AndyMik90/Auto-Claude

Length of output: 1373


🏁 Script executed:

# Search for validateClaude calls and how claudeCmd parameter is passed
rg "validateClaude|validateClaudeAsync" -B 5 -A 2 apps/frontend/src/

Repository: AndyMik90/Auto-Claude

Length of output: 12351


🏁 Script executed:

# Look for where claudeCmd is set/determined in the class
rg "claudeCmd\s*=" -B 3 -A 3 apps/frontend/src/main/cli-tool-manager.ts | head -50

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

rg "shouldUseShell" -B 3 -A 5 apps/frontend/src/main/cli-tool-manager.ts

Repository: AndyMik90/Auto-Claude

Length of output: 890


🏁 Script executed:

# Check where userConfig.claudePath is set/loaded
rg "userConfig\s*=" -B 5 -A 5 apps/frontend/src/main/cli-tool-manager.ts | head -60

Repository: AndyMik90/Auto-Claude

Length of output: 370


🏁 Script executed:

# Search for any path validation logic
rg "isWrongPlatformPath|validate.*path|sanitize|escape" -B 2 -A 2 apps/frontend/src/main/cli-tool-manager.ts

Repository: AndyMik90/Auto-Claude

Length of output: 2610


🏁 Script executed:

fd "env-utils" -t f apps/frontend/src/main/

Repository: AndyMik90/Auto-Claude

Length of output: 240


🏁 Script executed:

# Look at env-utils to understand shouldUseShell
cat -n apps/frontend/src/main/env-utils.ts | grep -A 15 "shouldUseShell"

Repository: AndyMik90/Auto-Claude

Length of output: 1114


🏁 Script executed:

# Check what ToolConfig contains and how it's defined
rg "interface ToolConfig|type ToolConfig" -A 10 apps/frontend/src/

Repository: AndyMik90/Auto-Claude

Length of output: 742


🏁 Script executed:

# Look for where userConfig is initialized
rg "this.userConfig\s*=|new.*ToolConfig|ToolConfig\(" -B 2 -A 2 apps/frontend/src/main/cli-tool-manager.ts | head -40

Repository: AndyMik90/Auto-Claude

Length of output: 234


🏁 Script executed:

# Search for where configure() is called and what sources populate userConfig
rg "\.configure\(|loadConfig|readConfig|userConfig" -B 3 -A 3 apps/frontend/src/main/cli-tool-manager.ts | head -80

Repository: AndyMik90/Auto-Claude

Length of output: 2971


🏁 Script executed:

# Check if there's any settings/config file handling
fd "settings|config" -t f apps/frontend/src/main/*.ts | head -10

Repository: AndyMik90/Auto-Claude

Length of output: 3000


🏁 Script executed:

# Look for where user input is taken (UI components for path input)
rg "claudePath|pythonPath|gitPath" apps/frontend/src/ -l | grep -v test | head -5

Repository: AndyMik90/Auto-Claude

Length of output: 316


🏁 Script executed:

# Check the IPC handlers or settings storage
rg "ipcMain|settings|store" -B 2 -A 2 apps/frontend/src/main/ | grep -A 5 -B 5 "claudePath\|toolConfig" | head -60

Repository: AndyMik90/Auto-Claude

Length of output: 3064


🏁 Script executed:

cat -n apps/frontend/src/main/ipc-handlers/settings-handlers.ts | head -100

Repository: AndyMik90/Auto-Claude

Length of output: 4873


🏁 Script executed:

# Look for claudePath validation in settings
rg "claudePath" -B 5 -A 5 apps/frontend/src/main/ipc-handlers/settings-handlers.ts

Repository: AndyMik90/Auto-Claude

Length of output: 1886


🏁 Script executed:

# Check the settings UI component to see if user can directly input paths
fd "settings" -t f apps/frontend/src/renderer/ | head -10

Repository: AndyMik90/Auto-Claude

Length of output: 767


🏁 Script executed:

# Look for form validation in settings
rg "claudePath" -B 3 -A 3 apps/frontend/src/renderer/

Repository: AndyMik90/Auto-Claude

Length of output: 1872


🏁 Script executed:

# Verify the Input component has no validation
cat -n apps/frontend/src/renderer/components/settings/GeneralSettings.tsx | grep -B 10 -A 10 "claudePath"

Repository: AndyMik90/Auto-Claude

Length of output: 1542


🏁 Script executed:

# Check if there's any sanitization/validation when saving settings
rg "SETTINGS_SAVE" -B 5 -A 15 apps/frontend/src/main/ipc-handlers/settings-handlers.ts | head -80

Repository: AndyMik90/Auto-Claude

Length of output: 1803


Critical: Command injection vulnerability in shell-string interpolation for user-configured Claude CLI paths.

User-provided claudePath from settings (via GeneralSettings.tsx input field with no validation) is interpolated directly into execSync() shell commands when the path ends in .cmd or .bat. A malicious path like C:\evil" && calc.exe && echo " breaks out of the quote wrapper and executes arbitrary commands. This applies to both validateClaude() (lines 931–954) and validateClaudeAsync() (lines 1048–1072).

Restrict interpolation of claudeCmd into shell commands by validating against shell metacharacters before use. The proposed regex check (/["\r\n]/ and /[;&|$(){}[]<>!^]/`) provides reasonable protection but consider:

  • Whether shell execution can be avoided entirely (use execFileSync with array args instead)
  • Or apply proper shell escaping via a library like shell-escape rather than manual validation
🤖 Prompt for AI Agents
In @apps/frontend/src/main/cli-tool-manager.ts around lines 931 - 954, The code
interpolates user-controlled claudeCmd into an execSync shell string in
validateClaude() and validateClaudeAsync(), allowing shell injection; fix by
never building a shell command string with the user path — use
execFileSync(claudeCmd, ['--version'], {...}) for all cases (or execFileSync
with shell:true if Windows requires cmd processing) so arguments are passed
separately, and add a defensive validation on claudeCmd in both validateClaude
and validateClaudeAsync (reject quotes, newlines and shell metacharacters like
;&|`$(){}[]<>!^ or use a proven escaping library such as shell-escape) before
executing.

Comment on lines +13 to 14
import { getSpawnOptions } from '../env-utils';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd apps/frontend/src/main/ipc-handlers && ls -la | head -20

Repository: AndyMik90/Auto-Claude

Length of output: 1338


🏁 Script executed:

wc -l apps/frontend/src/main/ipc-handlers/env-handlers.ts

Repository: AndyMik90/Auto-Claude

Length of output: 120


🏁 Script executed:

head -30 apps/frontend/src/main/ipc-handlers/env-handlers.ts

Repository: AndyMik90/Auto-Claude

Length of output: 1219


🏁 Script executed:

sed -n '600,620p' apps/frontend/src/main/ipc-handlers/env-handlers.ts

Repository: AndyMik90/Auto-Claude

Length of output: 726


🏁 Script executed:

sed -n '620,640p' apps/frontend/src/main/ipc-handlers/env-handlers.ts

Repository: AndyMik90/Auto-Claude

Length of output: 785


🏁 Script executed:

sed -n '690,710p' apps/frontend/src/main/ipc-handlers/env-handlers.ts

Repository: AndyMik90/Auto-Claude

Length of output: 814


🏁 Script executed:

cat apps/frontend/src/main/ipc-handlers/env-utils.ts | head -60

Repository: AndyMik90/Auto-Claude

Length of output: 145


🏁 Script executed:

fd -type f -name "env-utils.ts"

Repository: AndyMik90/Auto-Claude

Length of output: 235


🏁 Script executed:

rg "export.*getSpawnOptions" --type ts

Repository: AndyMik90/Auto-Claude

Length of output: 133


🏁 Script executed:

rg "function getSpawnOptions\|const getSpawnOptions" -A 10 --type ts

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

cat apps/frontend/src/main/env-utils.ts

Repository: AndyMik90/Auto-Claude

Length of output: 17524


🏁 Script executed:

rg "getClaudeCliInvocation|claude-cli-utils" -l

Repository: AndyMik90/Auto-Claude

Length of output: 547


🏁 Script executed:

cat apps/frontend/src/main/claude-cli-utils.ts | head -100

Repository: AndyMik90/Auto-Claude

Length of output: 2385


🏁 Script executed:

sed -n '1,150p' apps/frontend/src/main/claude-cli-utils.ts

Repository: AndyMik90/Auto-Claude

Length of output: 2385


🏁 Script executed:

rg "export function getToolPath|export const getToolPath" -A 15

Repository: AndyMik90/Auto-Claude

Length of output: 1174


🏁 Script executed:

cat apps/frontend/src/main/cli-tool-manager.ts | head -200

Repository: AndyMik90/Auto-Claude

Length of output: 6443


🏁 Script executed:

sed -n '200,400p' apps/frontend/src/main/cli-tool-manager.ts

Repository: AndyMik90/Auto-Claude

Length of output: 6572


🏁 Script executed:

sed -n '400,600p' apps/frontend/src/main/cli-tool-manager.ts | grep -A 30 "getToolPath\|validateClaude"

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

rg "detectClaude|private detectClaude" -A 50

Repository: AndyMik90/Auto-Claude

Length of output: 22064


Fix Windows spawn() with shell mode by quoting command paths.
On Windows, spawn() with shell: true requires quoted paths containing spaces. Since Claude CLI can be installed in paths like C:\Program Files\Claude\claude.cmd, the unquoted claudeCmd will fail. Align with how cli-tool-manager.ts handles this in validation.

Proposed diff (call-site quoting)
-          const proc = spawn(claudeCmd, ['--version'], getSpawnOptions(claudeCmd, {
+          const procOpts = getSpawnOptions(claudeCmd, {
             cwd: project.path,
             env: claudeEnv,
-          }));
+          });
+          const procCmd = procOpts.shell ? `"${claudeCmd}"` : claudeCmd;
+          const proc = spawn(procCmd, ['--version'], procOpts);
-              const authCheck = spawn(claudeCmd, ['api', '--help'], getSpawnOptions(claudeCmd, {
+              const authOpts = getSpawnOptions(claudeCmd, {
                 cwd: project.path,
                 env: claudeEnv,
-              }));
+              });
+              const authCmd = authOpts.shell ? `"${claudeCmd}"` : claudeCmd;
+              const authCheck = spawn(authCmd, ['api', '--help'], authOpts);
-          const proc = spawn(claudeCmd, ['setup-token'], getSpawnOptions(claudeCmd, {
+          const procOpts = getSpawnOptions(claudeCmd, {
             cwd: project.path,
             env: claudeEnv,
             stdio: 'inherit' // This allows the terminal to handle the interactive auth
-          }));
+          });
+          const procCmd = procOpts.shell ? `"${claudeCmd}"` : claudeCmd;
+          const proc = spawn(procCmd, ['setup-token'], procOpts);

Also applies to: 606-610, 626-629, 695-699

🤖 Prompt for AI Agents
In @apps/frontend/src/main/ipc-handlers/env-handlers.ts around lines 13 - 14,
The spawn call-sites are failing on Windows when shell: true because unquoted
command paths with spaces (e.g., claudeCmd) break; update each place that calls
spawn(...) with a command variable like claudeCmd (and other CLI command vars
referenced near the noted ranges) to wrap the command path in double quotes when
running on Windows and shell mode is enabled (mirror the quoting logic used in
cli-tool-manager.ts validation), e.g., build the spawn command as
`"${claudeCmd}"` before passing to spawn, ensuring getSpawnOptions or platform
detection is respected so non-Windows platforms are unchanged.

Use path component checks instead of full path string matching
to handle different path separators on different host OSes
(path.join uses host OS separator, not mocked process.platform)
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @apps/frontend/src/main/__tests__/cli-tool-manager.test.ts:
- Around line 703-717: The test only asserts that
findWindowsExecutableViaWhereAsync was called but doesn't confirm
getToolPathAsync returns a found path; add a new test that mocks
findWindowsExecutableViaWhereAsync to resolve to a valid path (e.g.,
'D:\\...\\claude.cmd'), mock existsSync to return true, ensure
findExecutableAsync still resolves to null, then call getToolPathAsync('claude')
and assert the returned value equals the mocked path; reference functions:
getToolPathAsync, findWindowsExecutableViaWhereAsync, findExecutableAsync, and
existsSync.
- Around line 362-372: Add a TODO or issue reference to the skipped test to
track future work: update the skipped test in the "User configuration priority"
suite (the it.skip('placeholder for user config tests') block in
cli-tool-manager.test.ts) to include a concise TODO comment or an issue/PR URL
and an owner/assignee so it’s discoverable (e.g., "TODO: implement user config
integration tests - see ISSUE-1234, assigned to @owner"); keep the skip but
ensure the comment includes the tracking ID and brief purpose.
- Around line 50-93: The mock uses untyped `any` parameters in the child_process
mocks (sharedSyncMock, mockExecFile, mockExec) causing linter warnings; update
the signatures to use concrete types (e.g., cmd: string, args?: string[] |
undefined, options?: Record<string, unknown> | undefined) and type the callback
as (error: Error | null, stdout: string, stderr: string) => void so
execFileSync/execFile/execSync/exec have proper parameter types while preserving
the same mock behavior and return values.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2c5978 and ca5fefc.

📒 Files selected for processing (1)
  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
apps/frontend/src/**/*.{ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use i18n translation keys for all user-facing text in the frontend instead of hardcoded strings

Files:

  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
apps/frontend/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use useTranslation() hook with namespace prefixes (e.g., 'navigation:items.key') for accessing translation strings in React components

Files:

  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
apps/frontend/**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

apps/frontend/**/*.{ts,tsx}: Review React patterns and TypeScript type safety.
Check for proper state management and component composition.

Files:

  • apps/frontend/src/main/__tests__/cli-tool-manager.test.ts
🪛 GitHub Check: test-frontend
apps/frontend/src/main/__tests__/cli-tool-manager.test.ts

[warning] 108-108:
Unexpected any. Specify a different type


[warning] 84-84:
Unexpected any. Specify a different type


[warning] 71-71:
Unexpected any. Specify a different type


[warning] 71-71:
Unexpected any. Specify a different type


[warning] 71-71:
Unexpected any. Specify a different type


[warning] 68-68:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: CodeQL (javascript-typescript)
  • GitHub Check: CodeQL (python)
🔇 Additional comments (4)
apps/frontend/src/main/__tests__/cli-tool-manager.test.ts (4)

141-299: LGTM!

The NVM path detection tests are well-structured with good coverage of:

  • Multiple Node version scenarios with proper sorting
  • Platform-specific behavior (skipping Unix NVM on Windows)
  • Graceful handling of missing directories
  • Fallback behavior when Claude isn't in the newest version
  • Validation failure scenarios

The use of regex patterns for path assertions (e.g., /v22\.17\.0[/\\]bin[/\\]claude/) properly handles cross-platform path separators.


378-525: LGTM!

The helper function tests provide solid coverage:

  • getClaudeDetectionPaths: Platform-specific path generation for macOS, Windows, and Linux
  • sortNvmVersionDirs: Descending version ordering, filtering non-version directories, handling malformed versions, and correct patch version comparison (v20.0.10 > v20.0.2)
  • buildClaudeDetectionResult: Null return on validation failure and proper result structure on success

719-732: LGTM!

The graceful error handling test correctly verifies that when all detection methods fail asynchronously, the function returns the fallback value 'claude' without crashing.


96-112: LGTM!

The env-utils mock correctly implements platform-aware behavior for shouldUseShell and getSpawnOptions. The mock functions reference process.platform at call time, which properly respects the platform modifications made by individual tests via Object.defineProperty.

Comment on lines +50 to 93
vi.mock('child_process', () => {
// Shared mock for sync execution - both execFileSync and execSync use this
// so when tests call vi.mocked(execFileSync).mockReturnValue(), it affects execSync too
const sharedSyncMock = vi.fn();

const mockExecFile = vi.fn((cmd: any, args: any, options: any, callback: any) => {
// Return a minimal ChildProcess-like object
const childProcess = {
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn()
};

// If callback is provided, call it asynchronously
if (typeof callback === 'function') {
setImmediate(() => callback(null, 'claude-code version 1.0.0\n', ''));
}

return childProcess as any;
});

const mockExec = vi.fn((cmd: any, options: any, callback: any) => {
// Return a minimal ChildProcess-like object
const childProcess = {
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn()
};

// If callback is provided, call it asynchronously
if (typeof callback === 'function') {
setImmediate(() => callback(null, 'claude-code version 1.0.0\n', ''));
}

return childProcess as any;
});

return {
existsSync: vi.fn(),
readdirSync: vi.fn(),
promises: {}
execFileSync: sharedSyncMock,
execFile: mockExecFile,
execSync: sharedSyncMock, // Share with execFileSync so tests work for both
exec: mockExec
};
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding proper types to mock function parameters to satisfy the linter.

The static analysis tool flags multiple any types in the mock callback signatures. While using any in test mocks is common, adding proper types improves maintainability.

🔧 Suggested type improvements
-  const mockExecFile = vi.fn((cmd: any, args: any, options: any, callback: any) => {
+  const mockExecFile = vi.fn((
+    cmd: string,
+    args: string[] | null,
+    options: Record<string, unknown> | ((err: Error | null, stdout: string, stderr: string) => void),
+    callback?: (err: Error | null, stdout: string, stderr: string) => void
+  ) => {
     // Return a minimal ChildProcess-like object
     const childProcess = {
       stdout: { on: vi.fn() },
       stderr: { on: vi.fn() },
       on: vi.fn()
     };

-    // If callback is provided, call it asynchronously
-    if (typeof callback === 'function') {
+    // Handle both 3-arg and 4-arg signatures
+    const cb = typeof options === 'function' ? options : callback;
+    if (typeof cb === 'function') {
       setImmediate(() => callback(null, 'claude-code version 1.0.0\n', ''));
     }

-    return childProcess as any;
+    return childProcess as unknown;
   });

-  const mockExec = vi.fn((cmd: any, options: any, callback: any) => {
+  const mockExec = vi.fn((
+    cmd: string,
+    options: Record<string, unknown> | ((err: Error | null, stdout: string, stderr: string) => void),
+    callback?: (err: Error | null, stdout: string, stderr: string) => void
+  ) => {

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Check: test-frontend

[warning] 84-84:
Unexpected any. Specify a different type


[warning] 71-71:
Unexpected any. Specify a different type


[warning] 71-71:
Unexpected any. Specify a different type


[warning] 71-71:
Unexpected any. Specify a different type


[warning] 68-68:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type


[warning] 55-55:
Unexpected any. Specify a different type

🤖 Prompt for AI Agents
In @apps/frontend/src/main/__tests__/cli-tool-manager.test.ts around lines 50 -
93, The mock uses untyped `any` parameters in the child_process mocks
(sharedSyncMock, mockExecFile, mockExec) causing linter warnings; update the
signatures to use concrete types (e.g., cmd: string, args?: string[] |
undefined, options?: Record<string, unknown> | undefined) and type the callback
as (error: Error | null, stdout: string, stderr: string) => void so
execFileSync/execFile/execSync/exec have proper parameter types while preserving
the same mock behavior and return values.

Comment on lines 362 to 372
describe('User configuration priority', () => {
beforeEach(() => {
vi.clearAllMocks();
});

// Note: User configuration testing requires mocking the CLIToolManager class instance
// which is more complex. These would be integration tests rather than unit tests.
it.skip('placeholder for user config tests', () => {
// Placeholder - actual user config tests require different approach
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding a TODO or issue reference for the skipped test.

The skipped test is documented as a placeholder, but tracking it with a TODO comment or issue reference would help ensure it's addressed in a future PR.

   describe('User configuration priority', () => {
     beforeEach(() => {
       vi.clearAllMocks();
     });

     // Note: User configuration testing requires mocking the CLIToolManager class instance
     // which is more complex. These would be integration tests rather than unit tests.
+    // TODO: Create integration tests for user configuration priority (issue #XXX)
     it.skip('placeholder for user config tests', () => {
       // Placeholder - actual user config tests require different approach
     });
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('User configuration priority', () => {
beforeEach(() => {
vi.clearAllMocks();
});
// Note: User configuration testing requires mocking the CLIToolManager class instance
// which is more complex. These would be integration tests rather than unit tests.
it.skip('placeholder for user config tests', () => {
// Placeholder - actual user config tests require different approach
});
});
describe('User configuration priority', () => {
beforeEach(() => {
vi.clearAllMocks();
});
// Note: User configuration testing requires mocking the CLIToolManager class instance
// which is more complex. These would be integration tests rather than unit tests.
// TODO: Create integration tests for user configuration priority (issue #XXX)
it.skip('placeholder for user config tests', () => {
// Placeholder - actual user config tests require different approach
});
});
🤖 Prompt for AI Agents
In @apps/frontend/src/main/__tests__/cli-tool-manager.test.ts around lines 362 -
372, Add a TODO or issue reference to the skipped test to track future work:
update the skipped test in the "User configuration priority" suite (the
it.skip('placeholder for user config tests') block in cli-tool-manager.test.ts)
to include a concise TODO comment or an issue/PR URL and an owner/assignee so
it’s discoverable (e.g., "TODO: implement user config integration tests - see
ISSUE-1234, assigned to @owner"); keep the skip but ensure the comment includes
the tracking ID and brief purpose.

Comment on lines +703 to +717
it('should detect Claude CLI via where.exe asynchronously', async () => {
vi.mocked(os.homedir).mockReturnValue('C:\\Users\\test');

vi.mocked(findExecutableAsync).mockResolvedValue(null);

vi.mocked(findWindowsExecutableViaWhereAsync).mockResolvedValue(null);

// Mock file system - no platform paths exist
vi.mocked(existsSync).mockReturnValue(false);

await getToolPathAsync('claude');

// Verify where.exe was called on Windows
expect(findWindowsExecutableViaWhereAsync).toHaveBeenCalledWith('claude', '[Claude CLI]');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Test only verifies the function was called, not the detection result.

This test verifies that findWindowsExecutableViaWhereAsync is called with the correct arguments, but it doesn't verify the actual detection behavior when where.exe finds a path. Consider adding a test that mocks a successful where.exe result and verifies the returned path.

🧪 Add a test for successful async detection
it('should return Claude CLI path detected via where.exe asynchronously', async () => {
  vi.mocked(os.homedir).mockReturnValue('C:\\Users\\test');

  vi.mocked(findExecutableAsync).mockResolvedValue(null);

  // Mock where.exe finds Claude in nvm-windows location
  vi.mocked(findWindowsExecutableViaWhereAsync).mockResolvedValue(
    'D:\\Program Files\\nvm4w\\nodejs\\claude.cmd'
  );

  // Mock file exists check and validation
  vi.mocked(existsSync).mockReturnValue(true);

  const result = await getToolPathAsync('claude');

  expect(result).toBe('D:\\Program Files\\nvm4w\\nodejs\\claude.cmd');
});
🤖 Prompt for AI Agents
In @apps/frontend/src/main/__tests__/cli-tool-manager.test.ts around lines 703 -
717, The test only asserts that findWindowsExecutableViaWhereAsync was called
but doesn't confirm getToolPathAsync returns a found path; add a new test that
mocks findWindowsExecutableViaWhereAsync to resolve to a valid path (e.g.,
'D:\\...\\claude.cmd'), mock existsSync to return true, ensure
findExecutableAsync still resolves to null, then call getToolPathAsync('claude')
and assert the returned value equals the mocked path; reference functions:
getToolPathAsync, findWindowsExecutableViaWhereAsync, findExecutableAsync, and
existsSync.

@g1331 g1331 requested a review from AndyMik90 January 9, 2026 03:55
@g1331
Copy link
Contributor Author

g1331 commented Jan 9, 2026

Relationship with PR #841

I noticed #841 was opened after this PR and addresses a similar issue (Windows paths with spaces). Here's a comparison of the two PRs:

This PR (#827) focuses on:

  • Windows where.exe fallback detection - Finds Claude CLI when installed via nvm-windows or custom paths not in PATH
  • execSync for .cmd/.bat files - Handles paths with spaces correctly when shell execution is needed
  • Async detection improvements - Non-blocking Claude CLI detection

PR #841 focuses on:

  • Python subprocess shell handling - shell=True for .cmd/.bat in formatter/version-suggester
  • Security fixes - shlex.quote() for shell injection prevention
  • Code refactoring - Shared utility functions

Overlapping files:

  • cli-tool-manager.ts
  • env-utils.ts
  • Related test files

Suggestion:

These PRs are complementary rather than duplicates. The where.exe detection in this PR is a key feature not covered by #841. If maintainers agree, one could be merged first, then the other rebased to resolve conflicts.

@StillKnotKnown StillKnotKnown added refactor Code improvement, no new features priority/medium Normal priority in-progress Someone is working on it os/windows Windows specific 🔄 Checking Checking PR Status size/L Large (500-999 lines) v2.7.3 v2.7.4 labels Jan 9, 2026
@StillKnotKnown
Copy link
Collaborator

Hi @g1331,

Thanks for this. We are looking into this now

AlexMadera
AlexMadera previously approved these changes Jan 9, 2026
Copy link
Collaborator

@AlexMadera AlexMadera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Auto Claude Review - APPROVED

Status: Ready to Merge

Summary: ### Merge Verdict: ✅ READY TO MERGE

✅ Ready to merge - All checks passing, no blocking issues found.

No blocking issues found

Risk Assessment

Factor Level Notes
Complexity High Based on lines changed
Security Impact None Based on security findings
Scope Coherence Good Based on structural review

Generated by Auto Claude PR Review


This automated review found no blocking issues. The PR can be safely merged.

Generated by Auto Claude

Copy link
Owner

@AndyMik90 AndyMik90 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Claude PR Review

Merge Verdict: 🟠 NEEDS REVISION

🟠 Needs revision - 4 issue(s) require attention.

4 issue(s) must be addressed (2 required, 2 recommended), 2 suggestions

Risk Assessment

Factor Level Notes
Complexity High Based on lines changed
Security Impact Medium Based on security findings
Scope Coherence Good Based on structural review

Findings Summary

  • High: 2 issue(s)
  • Medium: 2 issue(s)
  • Low: 2 issue(s)

Generated by Auto Claude PR Review

Findings (6 selected of 6 total)

🟠 [428b79bbf1ed] [HIGH] isSecurePath() does not block double-quote characters enabling shell escape

📁 apps/frontend/src/main/utils/windows-paths.ts:42

The isSecurePath() function validates paths against shell metacharacters but does NOT block double-quote characters. Since the code uses double-quote quoting in execSync ("${claudeCmd}"), paths containing double quotes can break out of the quoted context. A path like C:\test" & whoami & " would pass isSecurePath() validation but enable command injection when executed with shell:true.

Suggested fix:

Add double-quote character to the dangerous patterns regex: /[;&|`$(){}[\]<>!"]/

🟠 [7438a42fab19] [HIGH] User-configured Claude path bypasses isSecurePath() validation

📁 apps/frontend/src/main/cli-tool-manager.ts:726

User-configured paths from this.userConfig.claudePath are passed directly to validateClaude() without security validation. While paths from findWindowsExecutableViaWhere() are validated with isSecurePath(), user-configured paths only check isWrongPlatformPath() for platform compatibility, not security. A malicious path in settings.json could enable command injection.

Suggested fix:

Apply isSecurePath() validation to user-configured paths before calling validateClaude(): if (process.platform === 'win32' && !isSecurePath(this.userConfig.claudePath)) { console.warn('Path failed security validation'); } else { proceed with validation }

🟡 [503f0ed3803b] [MEDIUM] .bat files excluded from extension preference in where.exe results

📁 apps/frontend/src/main/utils/windows-paths.ts:163

The preference logic uses regex /.(cmd|exe)$/i which does NOT include .bat files. If where.exe returns an extensionless path first and a .bat file second, the extensionless path is returned even though .bat should be preferred. This is inconsistent with shouldUseShell() which correctly handles .bat files.

Suggested fix:

Change regex from /\.(cmd|exe)$/i to /\.(cmd|bat|exe)$/i at lines 163 and 266 (async version)

🟡 [9fa0de0dd7f8] [MEDIUM] Async version has same .bat extension preference bug

📁 apps/frontend/src/main/utils/windows-paths.ts:266

The async version findWindowsExecutableViaWhereAsync() has the same issue as the sync version - the regex /.(cmd|exe)$/i excludes .bat files from the preferred extensions when where.exe returns multiple results.

Suggested fix:

Change regex from /\.(cmd|exe)$/i to /\.(cmd|bat|exe)$/i

🔵 [0ac067b37f31] [LOW] Duplicate existsAsync function should be consolidated

📁 apps/frontend/src/main/cli-tool-manager.ts:42

The existsAsync function is defined identically in both env-utils.ts (lines 29-36) and cli-tool-manager.ts (lines 42-49). Both implementations use fsPromises.access to check file existence. This duplication creates maintenance risk if the implementation needs to change.

Suggested fix:

Export existsAsync from env-utils.ts and import it in cli-tool-manager.ts. Remove the duplicate definition.

🔵 [d8b7f452c659] [LOW] Skipped test placeholder provides no value

📁 apps/frontend/src/main/__tests__/cli-tool-manager.test.ts:369

The test it.skip('placeholder for user config tests', ...) is marked as skipped with a note that 'actual user config tests require different approach'. This is effectively dead code that provides no testing value and clutters the test file.

Suggested fix:

Either implement the user configuration tests properly, or remove the placeholder entirely. If planned for future, track with a GitHub issue instead.

This review was generated by Auto Claude.

env: claudeEnv,
shell: false
});
}));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spawn calls fail for Windows paths with spaces

High Severity

The spawn calls pass claudeCmd without quoting, but validateClaude in cli-tool-manager.ts correctly quotes the path with "${claudeCmd}" for execSync. When spawn runs with shell: true on Windows, paths containing spaces (like D:\Program Files\nodejs\claude.cmd) are misinterpreted because cmd.exe treats D:\Program as the command. Detection and validation will succeed (using quoted execSync), but actual execution will fail with "'D:\Program' is not recognized". This defeats the PR's purpose of fixing space-containing paths.

Additional Locations (2)

Fix in Cursor Fix in Web

Security fixes (HIGH):
- Add double quote ("), caret (^) to isSecurePath() dangerous chars
- Add Windows environment variable expansion pattern (%VAR%) detection
- Apply isSecurePath() validation to user-configured claudePath on Windows

Bug fixes (MEDIUM):
- Include .bat extension in where.exe result preference regex

Code quality (LOW):
- Export existsAsync from env-utils.ts, remove duplicate in cli-tool-manager.ts
- Remove unused test placeholder (it.skip for user config tests)
- Add existsAsync mock to env-utils mock in test file

All changes reviewed via Codex security audit.
The terminal-copy-paste.test.ts uses jsdom environment and imports
useXterm hook which calls requestAnimationFrame for initial terminal
fit. jsdom doesn't provide this function by default, causing CI
failure on Linux.

This adds requestAnimationFrame/cancelAnimationFrame mocks to the
test setup file, matching the existing scrollIntoView polyfill pattern.
@g1331
Copy link
Contributor Author

g1331 commented Jan 9, 2026

Changes Addressed

All 6 issues from the previous review have been fixed:

Security Fixes (HIGH)

  • ✅ Added double quote ("), caret (^), and %VAR% pattern detection to isSecurePath() in windows-paths.ts:42
  • ✅ Added isSecurePath() validation for user-configured claudePath in both detectClaude() and detectClaudeAsync()

Functionality Fixes (MEDIUM)

  • ✅ Added .bat to extension preference regex in findWindowsExecutableViaWhere() (line 163)
  • ✅ Added .bat to extension preference regex in findWindowsExecutableViaWhereAsync() (line 266)

Code Quality Fixes (LOW)

  • ✅ Consolidated duplicate existsAsync function - now exported from env-utils.ts and imported in cli-tool-manager.ts
  • ✅ Removed test placeholder it.skip('placeholder for user config tests')

Additional Fix

  • ✅ Added requestAnimationFrame polyfill to test setup for jsdom environment (fixed CI failure)

CI Status: All checks passing ✅

Ready for re-review. @AndyMik90

Copy link
Owner

@AndyMik90 AndyMik90 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Claude PR Review

🟠 Follow-up Review: Needs Revision

🟠 Needs revision - 1 blocking issue(s) require fixes.

Resolution Status

  • Resolved: 6 previous findings addressed
  • Unresolved: 0 previous findings remain
  • 🆕 New Issues: 1 new findings in recent changes

Finding Validation

  • 🔍 Dismissed as False Positives: 3 findings were re-investigated and found to be incorrect
  • Confirmed Valid: 1 findings verified as genuine issues
  • 👤 Needs Human Review: 0 findings require manual verification

🚨 Blocking Issues

  • quality: isSecurePath blocks legitimate Windows paths containing parentheses

Verdict

All 16 CI checks passing. All 6 previous findings (2 high, 2 medium, 2 low) have been successfully resolved - excellent work addressing security concerns. However, 1 new HIGH severity bug was confirmed: isSecurePath() blocks parentheses in paths, which rejects legitimate Windows paths like 'C:\Program Files (x86)\...'. This is a common installation location. The bug affects user-configured paths and paths found via where.exe. Fix required: Remove () from the dangerous characters regex, as parentheses are safe in quoted Windows paths.

Review Process

Agents invoked: resolution-verifier, new-code-reviewer, comment-analyzer, finding-validator


This is an AI-generated follow-up review using parallel specialist analysis with finding validation.

Findings (1 selected of 1 total)

🟠 [LOGIC-001] [HIGH] isSecurePath blocks legitimate Windows paths containing parentheses

📁 apps/frontend/src/main/utils/windows-paths.ts:42

The isSecurePath function regex /[;&|`$(){}[]<>!"^]/ includes parentheses () as dangerous characters. This incorrectly rejects legitimate Windows paths like 'C:\Program Files (x86)\Claude\claude.exe'. The bug affects: 1) User-configured paths on Windows (line 709 in cli-tool-manager.ts), 2) Paths found via where.exe (line 167 in windows-paths.ts). Users with Claude installed in standard 'Program Files (x86)' location would have their path rejected.

Suggested fix:

Remove parentheses from the single-character blocklist. Parentheses are safe in Windows paths when properly quoted. Alternative: add specific patterns for command substitution like $() instead of blocking all parentheses.

This review was generated by Auto Claude.

@AndyMik90
Copy link
Owner

@g1331 looks like just one blocking issue before we can merge it! :) thanks for the fast fixes

…ions

Remove standalone parentheses () from isSecurePath() dangerous character
detection. Parentheses are safe in Windows paths when properly quoted with
double quotes, and are required to support standard installation locations
like 'C:\Program Files (x86)\Claude\claude.exe'.

Security analysis:
- $() command substitution still blocked ($ character is in blocklist)
- &|<> command separators still blocked
- " quote breaking still blocked
- %VAR% expansion still blocked
- All other shell metacharacters still blocked

The code always uses double-quoted paths when shell:true, making
parentheses safe as literal characters in cmd.exe context.

Signed-off-by: g1331 <[email protected]>
@g1331
Copy link
Contributor Author

g1331 commented Jan 9, 2026

Fix Applied: Parentheses Issue Resolved

Commit: bda8924

Change Made

Removed standalone parentheses () from isSecurePath() dangerous character detection:

// Before
/[;&|`$(){}[\]<>!"^]/

// After  
/[;&|`${}[\]<>!"^]/

Security Analysis Verified

Protection Still Active Why Safe
$() command substitution Yes $ is still blocked
`& <>` command separators Yes
" quote breaking Yes Still in blocklist
%VAR% expansion Yes Separate pattern
Parentheses in quoted paths Safe "path" context

Test Results

  • All 28 cli-tool-manager tests pass
  • Program Files (x86) paths now accepted

Ready for re-review. @AndyMik90

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @apps/frontend/src/main/utils/windows-paths.ts:
- Around line 159-164: Remove the redundant/incorrect comment above the paths
computation: the comment "// Prefer paths with .cmd or .exe extensions
(executable files)" is inconsistent with the subsequent behavior; update or
delete it so the single comment near the foundPath logic accurately documents
that .cmd, .bat, or .exe are preferred. Locate the variables result, paths and
foundPath in windows-paths.ts and either delete the top comment or replace it
with a concise note matching the logic in the find call that prefers
.cmd/.bat/.exe extensions.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 63e538e and bda8924.

📒 Files selected for processing (1)
  • apps/frontend/src/main/utils/windows-paths.ts
🧰 Additional context used
📓 Path-based instructions (3)
apps/frontend/src/**/*.{ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use i18n translation keys for all user-facing text in the frontend instead of hardcoded strings

Files:

  • apps/frontend/src/main/utils/windows-paths.ts
apps/frontend/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use useTranslation() hook with namespace prefixes (e.g., 'navigation:items.key') for accessing translation strings in React components

Files:

  • apps/frontend/src/main/utils/windows-paths.ts
apps/frontend/**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

apps/frontend/**/*.{ts,tsx}: Review React patterns and TypeScript type safety.
Check for proper state management and component composition.

Files:

  • apps/frontend/src/main/utils/windows-paths.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: MikeeBuilds
Repo: AndyMik90/Auto-Claude PR: 661
File: apps/frontend/src/renderer/components/onboarding/OllamaModelSelector.tsx:176-189
Timestamp: 2026-01-04T23:59:48.743Z
Learning: In the AndyMik90/Auto-Claude repository, pre-existing i18n issues (hardcoded user-facing strings that should be localized) can be deferred to future i18n cleanup passes rather than requiring immediate fixes in PRs that don't introduce new i18n violations.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: CodeQL (javascript-typescript)
  • GitHub Check: CodeQL (python)
🔇 Additional comments (1)
apps/frontend/src/main/utils/windows-paths.ts (1)

40-56: LGTM: Export and enhanced security patterns.

The export of isSecurePath enables reuse across modules. The expanded security checks appropriately:

  • Remove parentheses from dangerous characters (safe when paths are double-quoted—confirmed in cli-tool-manager.ts line 926-927 where quotedCmd = \"${claudeCmd}"``)
  • Add Windows environment variable pattern /%[^%]+%/ for defense-in-depth alongside expandWindowsPath's check at line 79
  • Cover shell metacharacters, directory traversal, and newlines

The regex syntax is correct: [\] properly matches literal [ and ], and other metacharacters are treated as literals within the character class.

Comment on lines +159 to +164
// Prefer paths with .cmd or .exe extensions (executable files)
const paths = result.split(/\r?\n/).filter(p => p.trim());

if (paths.length > 0) {
const foundPath = paths[0].trim();
// Prefer .cmd, .bat, or .exe extensions, otherwise take first path
const foundPath = (paths.find(p => /\.(cmd|bat|exe)$/i.test(p)) || paths[0]).trim();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Minor: Redundant comments with inconsistent details.

Lines 159 and 163 both document the extension preference logic, but line 159 omits .bat from its description. Consider removing the line 159 comment since line 163 more accurately describes the behavior.

The implementation logic itself is correct: preferring any executable extension (.cmd/.bat/.exe) over non-extension files supports the PR's goal of identifying which files need special shell handling on Windows.

📝 Suggested documentation cleanup
-    // 'where' returns multiple paths separated by newlines if found in multiple locations
-    // Prefer paths with .cmd or .exe extensions (executable files)
+    // 'where' returns multiple paths separated by newlines if found in multiple locations
     const paths = result.split(/\r?\n/).filter(p => p.trim());
 
     if (paths.length > 0) {
       // Prefer .cmd, .bat, or .exe extensions, otherwise take first path
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Prefer paths with .cmd or .exe extensions (executable files)
const paths = result.split(/\r?\n/).filter(p => p.trim());
if (paths.length > 0) {
const foundPath = paths[0].trim();
// Prefer .cmd, .bat, or .exe extensions, otherwise take first path
const foundPath = (paths.find(p => /\.(cmd|bat|exe)$/i.test(p)) || paths[0]).trim();
// Prefer paths with .cmd or .exe extensions (executable files)
const paths = result.split(/\r?\n/).filter(p => p.trim());
if (paths.length > 0) {
// Prefer .cmd, .bat, or .exe extensions, otherwise take first path
const foundPath = (paths.find(p => /\.(cmd|bat|exe)$/i.test(p)) || paths[0]).trim();
🤖 Prompt for AI Agents
In @apps/frontend/src/main/utils/windows-paths.ts around lines 159 - 164, Remove
the redundant/incorrect comment above the paths computation: the comment "//
Prefer paths with .cmd or .exe extensions (executable files)" is inconsistent
with the subsequent behavior; update or delete it so the single comment near the
foundPath logic accurately documents that .cmd, .bat, or .exe are preferred.
Locate the variables result, paths and foundPath in windows-paths.ts and either
delete the top comment or replace it with a concise note matching the logic in
the find call that prefers .cmd/.bat/.exe extensions.

@AndyMik90 AndyMik90 merged commit 11710c5 into AndyMik90:develop Jan 9, 2026
16 checks passed
@g1331 g1331 deleted the fix/claude-cli-detection-windows-where branch January 10, 2026 05:27
@coderabbitai coderabbitai bot mentioned this pull request Jan 14, 2026
arcker pushed a commit to arcker/Auto-Claude that referenced this pull request Jan 14, 2026
…ths (AndyMik90#827)

* fix: improve Claude CLI detection with Windows where.exe fallback

- Add where.exe as fallback detection method on Windows (step 4)
- Enables detection of Claude CLI in non-standard paths (e.g., nvm-windows)
- where.exe searches PATH + Windows Registry + current directory
- Add 8 comprehensive unit tests (6 sync + 2 async)
- Update JSDoc comments to reflect new detection priority

Fixes issue where Claude CLI installed via nvm-windows or other
non-standard locations cannot be detected by standard PATH search.
The where.exe utility is universally available on Windows and provides
more comprehensive executable resolution than basic PATH checks.

Signed-off-by: yc13 <[email protected]>

* fix: prefer .cmd/.exe extensions when where.exe returns multiple paths

When where.exe finds multiple paths for the same executable (e.g., both
'claude' and 'claude.cmd'), we now prefer paths with .cmd or .exe extensions
since Windows requires extensions to execute files.

This fixes Claude CLI detection for nvm-windows installations where the
executable is installed as claude.cmd but where.exe returns the extensionless
path first.

Signed-off-by: yc13 <[email protected]>

* fix: use execSync for .cmd/.bat files to handle paths with spaces on Windows

Root cause: execFileSync cannot handle paths with spaces in .cmd/.bat files,
even with shell:true. Windows requires shell to execute batch files.

Solution:
- Add shouldUseShell() utility to detect .cmd/.bat files
- Use execSync (not execFileSync) for .cmd/.bat files with quoted paths
- Use getSpawnOptions() for spawn() calls in env-handlers.ts
- Add comprehensive unit tests (15 test cases)

Technical details:
- execFileSync + shell:true: FAILS with space in path
- execSync with quoted path: WORKS correctly
- spawn with getSpawnOptions(): WORKS correctly

Files changed:
- env-utils.ts: Add shouldUseShell() and getSpawnOptions()
- cli-tool-manager.ts: Use execSync/execAsync for .cmd/.bat validation
- env-handlers.ts: Use getSpawnOptions() for spawn calls
- env-utils.test.ts: Add 15 unit tests

Fixes issue where Claude CLI in paths like 'D:\Program Files\nvm4w\nodejs\claude.cmd'
fails with error: 'D:\Program' is not recognized as internal or external command.

Signed-off-by: g1331 <[email protected]>

* fix: address PR review feedback - remove unused imports and fix comment numbering

- Remove unused imports (execFile, app, execSync, mockDirent)
- Fix duplicate step numbering in Claude CLI detection comments (5→6, 6→7)
- Add exec mock to child_process for async validation support
- Add shouldUseShell and getSpawnOptions mocks for Windows .cmd handling

* fix: make Windows AppData test cross-platform compatible

Use path component checks instead of full path string matching
to handle different path separators on different host OSes
(path.join uses host OS separator, not mocked process.platform)

* fix: address PR review security findings and code quality issues

Security fixes (HIGH):
- Add double quote ("), caret (^) to isSecurePath() dangerous chars
- Add Windows environment variable expansion pattern (%VAR%) detection
- Apply isSecurePath() validation to user-configured claudePath on Windows

Bug fixes (MEDIUM):
- Include .bat extension in where.exe result preference regex

Code quality (LOW):
- Export existsAsync from env-utils.ts, remove duplicate in cli-tool-manager.ts
- Remove unused test placeholder (it.skip for user config tests)
- Add existsAsync mock to env-utils mock in test file

All changes reviewed via Codex security audit.

* fix: add requestAnimationFrame polyfill for jsdom test environment

The terminal-copy-paste.test.ts uses jsdom environment and imports
useXterm hook which calls requestAnimationFrame for initial terminal
fit. jsdom doesn't provide this function by default, causing CI
failure on Linux.

This adds requestAnimationFrame/cancelAnimationFrame mocks to the
test setup file, matching the existing scrollIntoView polyfill pattern.

* fix: allow parentheses in Windows paths for Program Files (x86) locations

Remove standalone parentheses () from isSecurePath() dangerous character
detection. Parentheses are safe in Windows paths when properly quoted with
double quotes, and are required to support standard installation locations
like 'C:\Program Files (x86)\Claude\claude.exe'.

Security analysis:
- $() command substitution still blocked ($ character is in blocklist)
- &|<> command separators still blocked
- " quote breaking still blocked
- %VAR% expansion still blocked
- All other shell metacharacters still blocked

The code always uses double-quoted paths when shell:true, making
parentheses safe as literal characters in cmd.exe context.

Signed-off-by: g1331 <[email protected]>

---------

Signed-off-by: yc13 <[email protected]>
Signed-off-by: g1331 <[email protected]>
Co-authored-by: Andy <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔄 Checking Checking PR Status in-progress Someone is working on it os/windows Windows specific priority/medium Normal priority refactor Code improvement, no new features size/L Large (500-999 lines) stable-roadmap v2.7.3 v2.7.4

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants