fix(windows): complete System32 executable path fixes for where.exe and taskkill.exe#1715
fix(windows): complete System32 executable path fixes for where.exe and taskkill.exe#1715VDT-91 wants to merge 7 commits intoAndyMik90:developfrom
Conversation
…nd taskkill.exe Complete the Windows System32 executable path fixes started in AndyMik90#1659. The previous fix addressed where.exe in a few frontend files but missed several critical locations in both backend and frontend. Backend changes: - Add get_where_exe_path() helper in core/platform/__init__.py - Update auth.py, git_executable.py, gh_executable.py, glab_executable.py to use full path instead of bare 'where' command - Remove shell=True from subprocess calls (security improvement) Frontend changes: - Add getTaskkillExePath() helper in windows-paths.ts - Update platform/index.ts, subprocess-runner.ts, pty-daemon-client.ts to use full path for taskkill.exe - Update claude-code-handlers.ts to use getWhereExePath() - Add System32 to ESSENTIAL_SYSTEM_PATHS in env-utils.ts - Update process-kill.test.ts to mock the new helper Why full paths: - Works even when System32 isn't in PATH (GUI launch scenarios) - SystemRoot env var is a protected Windows system variable - Prevents PATH hijacking attacks - Removing shell=True prevents command injection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughReplaces literal/shell-invoked Windows commands with resolved System32 paths and platform-aware PATH handling: adds Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
Summary of ChangesHello @VDT-91, 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 reliability and security of the application on Windows by ensuring that critical system executables like Highlights
Changelog
Activity
Using Gemini Code AssistThe 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
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 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
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/backend/core/glab_executable.py (1)
137-138:⚠️ Potential issue | 🟡 MinorStale comment:
shell=Trueis no longer used.The comment mentions
shell=Truebut_run_where_command()now uses a list-based invocation withoutshell=True. Update the comment to reflect the current implementation.📝 Suggested fix
- # 5. Try 'where' command with shell=True (more reliable on Windows) + # 5. Try 'where' command to find glab in PATH return _run_where_command()apps/backend/core/gh_executable.py (1)
137-138:⚠️ Potential issue | 🟡 MinorStale comment:
shell=Trueis no longer used.Same issue as in
glab_executable.py- the comment mentionsshell=Truebut the implementation uses list-based invocation without shell.📝 Suggested fix
- # 5. Try 'where' command with shell=True (more reliable on Windows) + # 5. Try 'where' command to find gh in PATH return _run_where_command()apps/frontend/src/main/utils/windows-paths.ts (1)
141-143: 🧹 Nitpick | 🔵 TrivialConsider adding
windiras a fallback for consistency.The codebase has another fallback pattern in
apps/frontend/scripts/download-python.cjs(line 367) that includesprocess.env.windiras an additional fallback. For extra robustness, you could align these helpers:♻️ Optional enhancement for both functions
export function getWhereExePath(): string { - const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows'; + const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT || process.env.windir || 'C:\\Windows'; return path.join(systemRoot, 'System32', 'where.exe'); } export function getTaskkillExePath(): string { - const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows'; + const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT || process.env.windir || 'C:\\Windows'; return path.join(systemRoot, 'System32', 'taskkill.exe'); }Also applies to: 154-156
There was a problem hiding this comment.
Code Review
This is a great pull request that thoroughly addresses the issue of finding Windows System32 executables in GUI-launched contexts. The changes are consistent across both the frontend and backend, and the use of full paths for where.exe and taskkill.exe is a robust solution. The removal of shell=True from Python subprocess calls is also a welcome security improvement.
I've added a few suggestions to reduce code duplication by extracting the logic for finding the SystemRoot directory into shared helper functions. This will make the code more maintainable.
Overall, excellent work on this important fix for Windows users.
| def get_where_exe_path() -> str: | ||
| """Get full path to where.exe on Windows. | ||
|
|
||
| Using the full path ensures where.exe works even when System32 isn't in PATH, | ||
| which can happen in restricted environments or when the app doesn't inherit | ||
| the full system PATH. | ||
|
|
||
| Returns: | ||
| Full path to where.exe (e.g., C:\\Windows\\System32\\where.exe) | ||
| """ | ||
| system_root = os.environ.get( | ||
| "SystemRoot", os.environ.get("SYSTEMROOT", "C:\\Windows") | ||
| ) | ||
| return os.path.join(system_root, "System32", "where.exe") |
There was a problem hiding this comment.
To improve maintainability and reduce code duplication, the logic for getting the SystemRoot path could be extracted into a private helper function. This would also make it easy to reuse in get_comspec_path for consistency, although that's outside the scope of this PR.
| def get_where_exe_path() -> str: | |
| """Get full path to where.exe on Windows. | |
| Using the full path ensures where.exe works even when System32 isn't in PATH, | |
| which can happen in restricted environments or when the app doesn't inherit | |
| the full system PATH. | |
| Returns: | |
| Full path to where.exe (e.g., C:\\Windows\\System32\\where.exe) | |
| """ | |
| system_root = os.environ.get( | |
| "SystemRoot", os.environ.get("SYSTEMROOT", "C:\\Windows") | |
| ) | |
| return os.path.join(system_root, "System32", "where.exe") | |
| def _get_system_root() -> str: | |
| """Get the SystemRoot path, handling case variations and providing a fallback.""" | |
| return os.environ.get("SystemRoot", os.environ.get("SYSTEMROOT", "C:\\Windows")) | |
| def get_where_exe_path() -> str: | |
| """Get full path to where.exe on Windows. | |
| Using the full path ensures where.exe works even when System32 isn't in PATH, | |
| which can happen in restricted environments or when the app doesn't inherit | |
| the full system PATH. | |
| Returns: | |
| Full path to where.exe (e.g., C:\\Windows\\System32\\where.exe) | |
| """ | |
| return os.path.join(_get_system_root(), "System32", "where.exe") |
| const ESSENTIAL_SYSTEM_PATHS: Record<string, string[]> = { | ||
| unix: ['/usr/bin', '/bin', '/usr/sbin', '/sbin'], | ||
| win32: [ | ||
| `${process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows'}\\System32`, |
There was a problem hiding this comment.
This logic for getting the SystemRoot path is duplicated. As suggested in windows-paths.ts, you could create a shared getSystemRoot() helper function there, export it, and then import and use it here to keep the logic centralized. You would need to add an import like import { getSystemRoot } from '../utils/windows-paths';.
| `${process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows'}\\System32`, | |
| path.join(getSystemRoot(), 'System32'), |
| /** | ||
| * Get the full path to taskkill.exe. | ||
| * Using the full path ensures taskkill.exe works even when System32 isn't in PATH, | ||
| * which can happen in restricted environments or when Electron doesn't inherit | ||
| * the full system PATH. | ||
| * | ||
| * @returns Full path to taskkill.exe (e.g., C:\Windows\System32\taskkill.exe) | ||
| */ | ||
| export function getTaskkillExePath(): string { | ||
| const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows'; | ||
| return path.join(systemRoot, 'System32', 'taskkill.exe'); | ||
| } |
There was a problem hiding this comment.
The logic to determine the SystemRoot path is duplicated in getWhereExePath() and this new getTaskkillExePath() function. To improve maintainability, you could extract this into a shared helper function. This helper could also be exported and used in env-utils.ts to centralize the logic.
/**
* Get the Windows SystemRoot path, handling case variations and providing a fallback.
* @returns The SystemRoot path (e.g., C:\Windows)
*/
export function getSystemRoot(): string {
return process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows';
}
/**
* Get the full path to taskkill.exe.
* Using the full path ensures taskkill.exe works even when System32 isn't in PATH,
* which can happen in restricted environments or when Electron doesn't inherit
* the full system PATH.
*
* @returns Full path to taskkill.exe (e.g., C:\Windows\System32\taskkill.exe)
*/
export function getTaskkillExePath(): string {
return path.join(getSystemRoot(), 'System32', 'taskkill.exe');
}There was a problem hiding this comment.
Actionable comments posted: 1
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/ipc-handlers/github/utils/subprocess-runner.ts (1)
174-174:⚠️ Potential issue | 🟡 MinorInconsistent:
stopFnstill uses baretaskkillinstead ofgetTaskkillExePath().This invocation was not updated to use the full path, unlike the auth and billing failure handlers at lines 265 and 324. For consistency and to avoid PATH-related failures, this should also use
getTaskkillExePath().🔧 Proposed fix
if (!isWindows()) { process.kill(-child.pid, 'SIGTERM'); } else { - execFile('taskkill', ['/pid', String(child.pid), '/T', '/F'], () => {}); + execFile(getTaskkillExePath(), ['/pid', String(child.pid), '/T', '/F'], () => {}); }
🤖 Fix all issues with AI agents
In `@apps/frontend/src/main/env-utils.ts`:
- Around line 148-158: ESSENTIAL_SYSTEM_PATHS currently constructs the Windows
System32 path using a hardcoded backslash; change this to build the path with
joinPaths so it uses the platform path helper. Locate the ESSENTIAL_SYSTEM_PATHS
constant and replace the string interpolation that appends '\\System32' to
process.env.SystemRoot or process.env.SYSTEMROOT with a call to
joinPaths(process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows',
'System32'), ensuring you import joinPaths from the platform utilities and
preserve the fallback to 'C:\\Windows'.
| /** | ||
| * Essential system directories that must always be in PATH | ||
| * Required for core system functionality (e.g., /usr/bin/security for Keychain access) | ||
| * Required for core system functionality (e.g., /usr/bin/security for Keychain access on macOS, | ||
| * System32 for where.exe/taskkill.exe on Windows) | ||
| */ | ||
| const ESSENTIAL_SYSTEM_PATHS: string[] = ['/usr/bin', '/bin', '/usr/sbin', '/sbin']; | ||
| const ESSENTIAL_SYSTEM_PATHS: Record<string, string[]> = { | ||
| unix: ['/usr/bin', '/bin', '/usr/sbin', '/sbin'], | ||
| win32: [ | ||
| `${process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows'}\\System32`, | ||
| ], | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use joinPaths for System32 to avoid hardcoded separators.
The \\System32 suffix hardcodes a Windows separator instead of using the platform path helper. Please build the path with joinPaths for consistency and cross-platform hygiene.
♻️ Suggested fix
-import { isWindows, isUnix, getPathDelimiter, getNpmCommand } from './platform';
+import { isWindows, isUnix, getPathDelimiter, getNpmCommand, joinPaths } from './platform';
@@
const ESSENTIAL_SYSTEM_PATHS: Record<string, string[]> = {
unix: ['/usr/bin', '/bin', '/usr/sbin', '/sbin'],
win32: [
- `${process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows'}\\System32`,
+ joinPaths(process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows', 'System32'),
],
};As per coding guidelines: Cross-platform executable lookup and path handling must use findExecutable(), getPathDelimiter(), joinPaths(), and requiresShell() functions from platform modules. Never hardcode paths or platform-specific separators.
📝 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.
| /** | |
| * Essential system directories that must always be in PATH | |
| * Required for core system functionality (e.g., /usr/bin/security for Keychain access) | |
| * Required for core system functionality (e.g., /usr/bin/security for Keychain access on macOS, | |
| * System32 for where.exe/taskkill.exe on Windows) | |
| */ | |
| const ESSENTIAL_SYSTEM_PATHS: string[] = ['/usr/bin', '/bin', '/usr/sbin', '/sbin']; | |
| const ESSENTIAL_SYSTEM_PATHS: Record<string, string[]> = { | |
| unix: ['/usr/bin', '/bin', '/usr/sbin', '/sbin'], | |
| win32: [ | |
| `${process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows'}\\System32`, | |
| ], | |
| }; | |
| const ESSENTIAL_SYSTEM_PATHS: Record<string, string[]> = { | |
| unix: ['/usr/bin', '/bin', '/usr/sbin', '/sbin'], | |
| win32: [ | |
| joinPaths(process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows', 'System32'), | |
| ], | |
| }; |
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/env-utils.ts` around lines 148 - 158,
ESSENTIAL_SYSTEM_PATHS currently constructs the Windows System32 path using a
hardcoded backslash; change this to build the path with joinPaths so it uses the
platform path helper. Locate the ESSENTIAL_SYSTEM_PATHS constant and replace the
string interpolation that appends '\\System32' to process.env.SystemRoot or
process.env.SYSTEMROOT with a call to joinPaths(process.env.SystemRoot ||
process.env.SYSTEMROOT || 'C:\\Windows', 'System32'), ensuring you import
joinPaths from the platform utilities and preserve the fallback to
'C:\\Windows'.
AndyMik90
left a comment
There was a problem hiding this comment.
🤖 Auto Claude PR Review
Merge Verdict: 🟠 NEEDS REVISION
🟠 Needs revision - 3 issue(s) require attention.
3 issue(s) must be addressed (0 required, 3 recommended), 2 suggestions
Risk Assessment
| Factor | Level | Notes |
|---|---|---|
| Complexity | Low | Based on lines changed |
| Security Impact | None | Based on security findings |
| Scope Coherence | Good | Based on structural review |
Findings Summary
- Medium: 3 issue(s)
- Low: 2 issue(s)
Generated by Auto Claude PR Review
Findings (5 selected of 5 total)
🟡 [84c04f78174f] [MEDIUM] Missed bare 'taskkill' — incomplete refactoring in same file
📁 apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts:174
This PR updated taskkill to getTaskkillExePath() at lines 265 and 324 in this same file, but missed the third occurrence at line 174 inside the stopFn closure. This is the exact same issue the PR is trying to fix — bare taskkill will fail when System32 isn't in PATH. The import for getTaskkillExePath is already present at line 25, so this is a straightforward miss. All three usages in this file are semantically identical (kill process tree on Windows), and only 2 of 3 were updated.
Suggested fix:
Change line 174 to: execFile(getTaskkillExePath(), ['/pid', String(child.pid), '/T', '/F'], () => {});
🔵 [6e9520ff3687] [LOW] Stale comment still references removed shell=True
📁 apps/backend/core/gh_executable.py:137
The comment on line 137 reads '# 5. Try 'where' command with shell=True (more reliable on Windows)' but this PR removed shell=True from the actual _run_where_command() function (lines 57-63). The comment should be updated to reflect the new approach (full path to where.exe), consistent with the comment in git_executable.py line 129 which was properly updated to '# 4. Try 'where' command with full path (works even when System32 isn't in PATH)'.
Suggested fix:
Update comment to: # 5. Try 'where' command with full path (works even when System32 isn't in PATH)
🔵 [a26366090696] [LOW] Stale comment still references removed shell=True
📁 apps/backend/core/glab_executable.py:138
Same issue as gh_executable.py — the comment on line 138 reads '# 5. Try 'where' command with shell=True (more reliable on Windows)' but shell=True was removed from _run_where_command() in this PR. This is identical to the stale comment in gh_executable.py, suggesting a copy-paste pattern where both were missed during the comment update.
Suggested fix:
Update comment to: # 5. Try 'where' command with full path (works even when System32 isn't in PATH)
🟡 [76ee61a33822] [MEDIUM] Stale comment references removed shell=True
📁 apps/backend/core/gh_executable.py:137
The comment at line 137 says '# 5. Try 'where' command with shell=True (more reliable on Windows)' but this PR specifically removed shell=True from _run_where_command() and replaced the bare string command with get_where_exe_path(). The comment is now misleading since shell=True is no longer used. Should be updated to reference the full path approach instead.
Suggested fix:
Update comment to: # 5. Try 'where' command with full System32 path (works without PATH)
🟡 [7cb5235a83cd] [MEDIUM] Stale comment references removed shell=True
📁 apps/backend/core/glab_executable.py:138
Same issue as gh_executable.py - the comment at line 138 says '# 5. Try 'where' command with shell=True (more reliable on Windows)' but shell=True was removed from _run_where_command() by this PR. The comment is misleading and should reflect the new approach of using get_where_exe_path() with a full System32 path.
Suggested fix:
Update comment to: # 5. Try 'where' command with full System32 path (works without PATH)
This review was generated by Auto Claude.
- Fix missed bare 'taskkill' at subprocess-runner.ts:174 (stopFn closure) - Fix missed bare 'where' at mcp-handlers.ts:198 (MCP health check) - Update stale comments in gh_executable.py and glab_executable.py (removed incorrect "shell=True" reference, now matches git_executable.py) - Add error logging callback for taskkill in stopFn (was empty callback) - Improve MCP health check error messages with actionable diagnostics for ENOENT and EACCES errors on both Windows and Unix Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
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 (1)
apps/backend/core/gh_executable.py (1)
50-75: 🧹 Nitpick | 🔵 TrivialOptional: Consider extracting common executable discovery logic.
The
_run_where_command()pattern is now duplicated acrossgh_executable.py,glab_executable.py, andgit_executable.py. A future refactor could extract this into a shared helper:# In core/platform/__init__.py or a new module def run_where_command(executable_name: str) -> str | None: """Run Windows 'where' command to find an executable.""" ...This is not blocking for the current security-focused PR, but could reduce duplication going forward.
🤖 Fix all issues with AI agents
In `@apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts`:
- Line 25: The current getTaskkillExePath helper builds a System32 path using
path.join and a hardcoded "C:\\Windows", which bypasses the platform helpers;
update the getTaskkillExePath implementation to use the platform abstraction
functions (joinPaths, getPathDelimiter, findExecutable and requiresShell) to
construct and locate taskkill.exe instead of hardcoding paths or separators, and
keep subprocess-runner.ts using getTaskkillExePath as-is; ensure any other
occurrences noted (lines ~174-176, 267-269, 326-328) are refactored similarly so
executable lookup calls rely on findExecutable and path construction uses
joinPaths/getPathDelimiter.
In `@apps/frontend/src/main/ipc-handlers/mcp-handlers.ts`:
- Around line 198-201: Replace the spawn-based which/where lookup with the
cross-platform helper: import findExecutableAsync from ./env-utils alongside
existing env-utils imports, remove use of getWhereExePath()/spawn, and call
await findExecutableAsync(server.command!) instead; then treat a non-null return
as "found" (rather than checking proc exit code), and handle the null case as
"not found" (preserving existing timeout/error handling semantics). Ensure
references to spawn and getWhereExePath() in this async handler (the block using
server.command!) are removed or replaced so only findExecutableAsync controls
the executable lookup.
| import { getClaudeProfileManager } from '../../../claude-profile-manager'; | ||
| import { getOperationRegistry, type OperationType } from '../../../claude-profile/operation-registry'; | ||
| import { isWindows, isMacOS } from '../../../platform'; | ||
| import { getTaskkillExePath } from '../../../utils/windows-paths'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Align taskkill path resolution with platform helpers.
getTaskkillExePath (apps/frontend/src/main/utils/windows-paths.ts Lines 153-156) builds the System32 path via path.join and a hardcoded C:\Windows fallback, which bypasses the required platform helpers for executable lookup and path handling. Please refactor the helper to use platform joinPaths/getPathDelimiter (and findExecutable/requiresShell if your platform API expects it) and keep using the helper here.
♻️ Example adjustment in windows-paths.ts
-import path from 'path';
+import { joinPaths, getPathDelimiter } from '../platform';
export function getTaskkillExePath(): string {
- const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows';
- return path.join(systemRoot, 'System32', 'taskkill.exe');
+ const systemRoot =
+ process.env.SystemRoot ||
+ process.env.SYSTEMROOT ||
+ `C:${getPathDelimiter()}Windows`;
+ return joinPaths(systemRoot, 'System32', 'taskkill.exe');
}As per coding guidelines, cross-platform executable lookup and path handling must use findExecutable(), getPathDelimiter(), joinPaths(), and requiresShell() functions from platform modules, and never hardcode paths or platform-specific separators.
Also applies to: 174-176, 267-269, 326-328
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts` at
line 25, The current getTaskkillExePath helper builds a System32 path using
path.join and a hardcoded "C:\\Windows", which bypasses the platform helpers;
update the getTaskkillExePath implementation to use the platform abstraction
functions (joinPaths, getPathDelimiter, findExecutable and requiresShell) to
construct and locate taskkill.exe instead of hardcoding paths or separators, and
keep subprocess-runner.ts using getTaskkillExePath as-is; ensure any other
occurrences noted (lines ~174-176, 267-269, 326-328) are refactored similarly so
executable lookup calls rely on findExecutable and path construction uses
joinPaths/getPathDelimiter.
| const command = isWindows() ? getWhereExePath() : 'which'; | ||
| const proc = spawn(command, [server.command!], { | ||
| timeout: 5000, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find apps/frontend/src/main/platform -type f -name "*.ts" | head -20Repository: AndyMik90/Auto-Claude
Length of output: 309
🏁 Script executed:
rg -n "findExecutable" apps/frontend/src/main/platform -g "*.ts"Repository: AndyMik90/Auto-Claude
Length of output: 141
🏁 Script executed:
rg -n "export function findExecutable\|export const findExecutable" apps/frontend/src/main/platform -g "*.ts"Repository: AndyMik90/Auto-Claude
Length of output: 47
🏁 Script executed:
cat -n apps/frontend/src/main/ipc-handlers/mcp-handlers.ts | sed -n '190,210p'Repository: AndyMik90/Auto-Claude
Length of output: 764
🏁 Script executed:
cat -n apps/frontend/src/main/platform/index.ts | sed -n '350,390p'Repository: AndyMik90/Auto-Claude
Length of output: 1380
🏁 Script executed:
rg -n "getWhereExePath" apps/frontend/src/main/platform -g "*.ts" -A 3Repository: AndyMik90/Auto-Claude
Length of output: 583
🏁 Script executed:
cat -n apps/frontend/src/main/platform/index.ts | sed -n '1,50p'Repository: AndyMik90/Auto-Claude
Length of output: 1959
🏁 Script executed:
cat -n apps/frontend/src/main/ipc-handlers/mcp-handlers.ts | sed -n '195,220p'Repository: AndyMik90/Auto-Claude
Length of output: 931
🏁 Script executed:
rg -n "findExecutable" apps/frontend/src -g "*.ts" -B 2 -A 4 | head -60Repository: AndyMik90/Auto-Claude
Length of output: 5480
🏁 Script executed:
find apps/frontend/src/main -name "env-utils*" -type fRepository: AndyMik90/Auto-Claude
Length of output: 240
🏁 Script executed:
rg -n "findExecutableAsync" apps/frontend/src/main -g "*.ts" -B 2 -A 8 | head -80Repository: AndyMik90/Auto-Claude
Length of output: 6299
🏁 Script executed:
cat -n apps/frontend/src/main/env-utils.ts | sed -n '465,495p'Repository: AndyMik90/Auto-Claude
Length of output: 1159
🏁 Script executed:
cat -n apps/frontend/src/main/ipc-handlers/mcp-handlers.ts | sed -n '1,30p'Repository: AndyMik90/Auto-Claude
Length of output: 1419
Replace hardcoded which/where spawn with findExecutableAsync() from platform utilities.
The code hardcodes the which utility for POSIX systems and spawns an external process. Use findExecutableAsync() from env-utils instead, which provides cross-platform executable lookup without relying on external utilities. This aligns with the platform abstraction guidelines and removes the shell dependency.
Change the async handler pattern from spawn-based checking to direct path lookup:
- Call
findExecutableAsync(server.command!)instead of spawningwhich/where - Check if the result is not null (executable found) rather than checking exit code
- Import
findExecutableAsyncfrom./env-utilsalongside existing env-utils imports
Current code pattern (lines 198–201)
const command = isWindows() ? getWhereExePath() : 'which';
const proc = spawn(command, [server.command!], {
timeout: 5000,
});
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/ipc-handlers/mcp-handlers.ts` around lines 198 - 201,
Replace the spawn-based which/where lookup with the cross-platform helper:
import findExecutableAsync from ./env-utils alongside existing env-utils
imports, remove use of getWhereExePath()/spawn, and call await
findExecutableAsync(server.command!) instead; then treat a non-null return as
"found" (rather than checking proc exit code), and handle the null case as "not
found" (preserving existing timeout/error handling semantics). Ensure references
to spawn and getWhereExePath() in this async handler (the block using
server.command!) are removed or replaced so only findExecutableAsync controls
the executable lookup.
AndyMik90
left a comment
There was a problem hiding this comment.
🤖 Auto Claude PR Review
🟠 Follow-up Review: Needs Revision
🟠 Needs revision - 1 blocking issue(s) require fixes.
Resolution Status
- ✅ Resolved: 5 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: Missed bare 'where gh' — incomplete refactoring in same file
Verdict
All 21 CI checks are passing. All 5 previous findings have been verified as resolved — the contributor addressed every piece of feedback from the initial review. However, 1 new medium-severity finding (NEW-001) was confirmed valid: a bare 'where gh' call at line 617 in subprocess-runner.ts was missed in this refactoring. This is the exact class of issue the PR aims to fix, and the file already imports getTaskkillExePath from the same windows-paths module, making getWhereExePath readily available. Three other findings were dismissed as false positives after validation (interactive terminal context mitigates bare taskkill, Python os.environ is case-insensitive on Windows, SystemRoot never changes during process lifetime). CodeRabbit's duplication refactor suggestion is non-blocking. The one remaining fix is small and straightforward — import getWhereExePath and use it at line 617.
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)
🟡 [NEW-001] [MEDIUM] Missed bare 'where gh' — incomplete refactoring in same file
📁 apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts:617
The validateGitHubModule() function at line 617 uses bare 'where gh' via execAsync() instead of using getWhereExePath() from windows-paths.ts. This is a missed instance of the exact class of issue this PR fixes. The file already imports getTaskkillExePath from the same module (line 25) and uses it consistently at lines 174, 267, and 326, but getWhereExePath is not imported and 'where' is called bare. On Windows systems where System32 is not in the inherited PATH, this gh CLI validation check would fail even though gh is installed.
Suggested fix:
Import getWhereExePath from '../../../utils/windows-paths' alongside getTaskkillExePath, then replace line 617 with: const whichCommand = isWindows() ? `"${getWhereExePath()}" gh` : 'which gh'; Or better yet, use execFileAsync with getWhereExePath() as the command and ['gh'] as args to avoid shell invocation entirely.
This review was generated by Auto Claude.
Fix missed bare 'where gh' at line 617 in subprocess-runner.ts. This completes the System32 executable path refactoring. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
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 (1)
apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts (1)
631-633:⚠️ Potential issue | 🟡 Minor
gh auth statusat line 633 uses bareghinstead of the resolved path from verification.Line 617–618 properly resolves
where.exeon Windows to handle incomplete PATH, but line 633 invokesghby name without first capturing and reusing the resolved path. In the same restricted environment, this call would fail identically. UsefindExecutable('gh')(available inenv-utils.ts) to obtain and cache the gh path during verification, then pass it toexecAsyncor switch toexecFileAsyncwith the resolved path.
🤖 Fix all issues with AI agents
In `@apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts`:
- Around line 174-176: Extract the repeated execFile(getTaskkillExePath(),
['/pid', String(child.pid), '/T', '/F'], callback) pattern into a single helper
(e.g., killChildProcess or taskkillPid) and use it from the SubprocessRunner
code paths (stopFn, the auth-failure kill, and the billing-failure kill). The
helper should accept the pid and an optional reason/context string, call
execFile(getTaskkillExePath(), ['/pid', String(pid), '/T', '/F'], ...), and
centralize the warning log message so all three sites call
killChildProcess(child.pid, 'stopFn'|'auth-failure'|'billing-failure') instead
of repeating the execFile call and console.warn.
- Around line 617-618: Replace the shell-based whichCommand + execAsync
invocation with a direct execFile call to the where.exe binary: when isWindows()
is true call execFile(getWhereExePath(), ['gh']) (or the equivalent execFile
pattern used by taskkill calls) instead of constructing a quoted command string;
keep the existing execAsync usage for non-Windows or replace both branches
consistently so subprocess-runner uses execFile/execFileAsync (referencing
isWindows(), getWhereExePath(), whichCommand and execAsync) to remove shell
invocation.
| execFile(getTaskkillExePath(), ['/pid', String(child.pid), '/T', '/F'], (err: Error | null) => { | ||
| if (err) console.warn('[SubprocessRunner] taskkill error (process may have already exited):', err.message); | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider extracting the repeated taskkill invocation into a helper.
The identical execFile(getTaskkillExePath(), ['/pid', ...], callback) pattern appears three times (stopFn, auth-failure kill, billing-failure kill). A small local helper would reduce duplication and make future changes (e.g., adding a timeout or changing logging) a single-point edit.
♻️ Suggested helper
+/** Kill a Windows process tree by PID using taskkill.exe */
+function killWindowsProcessTree(pid: number): void {
+ execFile(getTaskkillExePath(), ['/pid', String(pid), '/T', '/F'], (err: Error | null) => {
+ if (err) console.warn('[SubprocessRunner] taskkill error (process may have already exited):', err.message);
+ });
+}Then replace each call site, e.g.:
- execFile(getTaskkillExePath(), ['/pid', String(child.pid), '/T', '/F'], (err: Error | null) => {
- if (err) console.warn('[SubprocessRunner] taskkill error (process may have already exited):', err.message);
- });
+ killWindowsProcessTree(child.pid);Also applies to: 267-269, 326-328
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts` around
lines 174 - 176, Extract the repeated execFile(getTaskkillExePath(), ['/pid',
String(child.pid), '/T', '/F'], callback) pattern into a single helper (e.g.,
killChildProcess or taskkillPid) and use it from the SubprocessRunner code paths
(stopFn, the auth-failure kill, and the billing-failure kill). The helper should
accept the pid and an optional reason/context string, call
execFile(getTaskkillExePath(), ['/pid', String(pid), '/T', '/F'], ...), and
centralize the warning log message so all three sites call
killChildProcess(child.pid, 'stopFn'|'auth-failure'|'billing-failure') instead
of repeating the execFile call and console.warn.
| const whichCommand = isWindows() ? `"${getWhereExePath()}" gh` : 'which gh'; | ||
| await execAsync(whichCommand); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use execFile instead of exec for where.exe — consistent with the taskkill calls and the PR's goal of removing shell invocations.
The taskkill calls in this file already use execFile (no shell). The where.exe call should follow the same pattern, especially since the PR objective explicitly includes removing shell=True. where.exe is a standalone executable, so no shell is needed on Windows.
♻️ Proposed fix
+const execFileAsync = promisify(execFile);
+
// 2. Check gh CLI installation (cross-platform)
try {
- const whichCommand = isWindows() ? `"${getWhereExePath()}" gh` : 'which gh';
- await execAsync(whichCommand);
+ if (isWindows()) {
+ await execFileAsync(getWhereExePath(), ['gh']);
+ } else {
+ await execAsync('which gh');
+ }
result.ghCliInstalled = true;🤖 Prompt for AI Agents
In `@apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts` around
lines 617 - 618, Replace the shell-based whichCommand + execAsync invocation
with a direct execFile call to the where.exe binary: when isWindows() is true
call execFile(getWhereExePath(), ['gh']) (or the equivalent execFile pattern
used by taskkill calls) instead of constructing a quoted command string; keep
the existing execAsync usage for non-Windows or replace both branches
consistently so subprocess-runner uses execFile/execFileAsync (referencing
isWindows(), getWhereExePath(), whichCommand and execAsync) to remove shell
invocation.
Summary
This PR completes the Windows System32 executable path fixes started in #1659. The previous fix addressed
where.exein a few frontend files, but missed several critical locations in both the backend and frontend where bare executable names (where,taskkill) were still being used.Background
The Problem:
When Auto Claude is launched from GUI contexts (Windows Start Menu, desktop shortcut, or file associations), the application may not inherit the full system PATH. This causes bare executable calls like
where gitortaskkill /pid ...to fail with "command not found" errors becauseC:\Windows\System32isn't in the PATH.Previous Fix (#1659):
PR #1659 introduced
getWhereExePath()inwindows-paths.tsand updated a few frontend locations. However, it missed:auth.py,git_executable.py,gh_executable.py,glab_executable.py) were still using barewhereorwhere.execommandstaskkillusage - Process termination code inplatform/index.ts,subprocess-runner.ts, andpty-daemon-client.tsused baretaskkillwhereusage -claude-code-handlers.tswas still using barewhereenv-utils.tsonly ensured Unix system paths, not Windows System32Changes
Backend
get_where_exe_path()helper incore/platform/__init__.pyauth.py,git_executable.py,gh_executable.py,glab_executable.pyto use full pathshell=Truefrom subprocess calls (security improvement)Frontend
getTaskkillExePath()helper inwindows-paths.tsplatform/index.ts,subprocess-runner.ts,pty-daemon-client.tsto use full path fortaskkill.execlaude-code-handlers.tsto usegetWhereExePath()ESSENTIAL_SYSTEM_PATHSinenv-utils.tsprocess-kill.test.tsto mock the new helperWhy Full Paths?
C:\Windows\System32\where.exeworks regardless of PATH configurationshell=Trueprevents command injection; full paths prevent PATH hijackingTest plan
Related
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
New Features
Tests