aa7f56e5d0
* fix(windows): complete System32 executable path fixes for where.exe and taskkill.exe Complete the Windows System32 executable path fixes started in #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> * fix: address PR review feedback for Windows System32 executable paths - 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> * fix: use getWhereExePath() for 'where gh' in validateGitHubModule 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> * fix: use execFileAsync for gh CLI detection consistency Replace shell string interpolation with execFileAsync for the gh CLI check in subprocess-runner.ts, matching the pattern used in claude-code-handlers.ts and mcp-handlers.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(windows): address PR review findings for System32 path consistency - Use getTaskkillExePath() in claude-code-handlers.ts install commands instead of bare 'taskkill' (NEW-003) - Add ENOENT error handling in subprocess-runner.ts validateGitHubModule to distinguish missing where.exe from missing gh CLI (NEW-006) - Extract shared getSystemRoot() helper in windows-paths.ts to deduplicate SystemRoot resolution logic (NEW-002) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(windows): add tests, export getSystemRoot, add windowsHide to MCP spawn - Add unit tests for getWhereExePath/getTaskkillExePath with env fallback coverage - Export getSystemRoot() for reuse across modules - Add windowsHide: true to mcp-handlers spawn call (consistency with other sites) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
200 lines
6.5 KiB
Python
200 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Git Executable Finder and Isolation
|
|
====================================
|
|
|
|
Utility to find the git executable, with Windows-specific fallbacks.
|
|
Also provides environment isolation to prevent pre-commit hooks and
|
|
other git configurations from affecting worktree operations.
|
|
|
|
Separated into its own module to avoid circular imports.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from core.platform import get_where_exe_path
|
|
|
|
# Git environment variables that can interfere with worktree operations
|
|
# when set by pre-commit hooks or other git configurations.
|
|
# These must be cleared to prevent cross-worktree contamination.
|
|
GIT_ENV_VARS_TO_CLEAR = [
|
|
"GIT_DIR",
|
|
"GIT_WORK_TREE",
|
|
"GIT_INDEX_FILE",
|
|
"GIT_OBJECT_DIRECTORY",
|
|
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
# Identity variables that could be set by hooks
|
|
"GIT_AUTHOR_NAME",
|
|
"GIT_AUTHOR_EMAIL",
|
|
"GIT_AUTHOR_DATE",
|
|
"GIT_COMMITTER_NAME",
|
|
"GIT_COMMITTER_EMAIL",
|
|
"GIT_COMMITTER_DATE",
|
|
]
|
|
|
|
_cached_git_path: str | None = None
|
|
|
|
|
|
def get_isolated_git_env(base_env: dict | None = None) -> dict:
|
|
"""
|
|
Create an isolated environment for git operations.
|
|
|
|
Clears git environment variables that may be set by pre-commit hooks
|
|
or other git configurations, preventing cross-worktree contamination
|
|
and ensuring git operations target the intended repository.
|
|
|
|
Args:
|
|
base_env: Base environment dict to copy from. If None, uses os.environ.
|
|
|
|
Returns:
|
|
Environment dict safe for git subprocess operations.
|
|
"""
|
|
env = dict(base_env) if base_env is not None else os.environ.copy()
|
|
|
|
for key in GIT_ENV_VARS_TO_CLEAR:
|
|
env.pop(key, None)
|
|
|
|
# Disable user's pre-commit hooks during Auto-Claude managed git operations
|
|
# to prevent double-hook execution and potential conflicts
|
|
env["HUSKY"] = "0"
|
|
|
|
return env
|
|
|
|
|
|
def get_git_executable() -> str:
|
|
"""Find the git executable, with Windows-specific fallbacks.
|
|
|
|
Returns the path to git executable. On Windows, checks multiple sources:
|
|
1. CLAUDE_CODE_GIT_BASH_PATH env var (set by Electron frontend)
|
|
2. shutil.which (if git is in PATH)
|
|
3. Common installation locations
|
|
4. Windows 'where' command
|
|
|
|
Caches the result after first successful find.
|
|
"""
|
|
global _cached_git_path
|
|
|
|
# Return cached result if available
|
|
if _cached_git_path is not None:
|
|
return _cached_git_path
|
|
|
|
git_path = _find_git_executable()
|
|
_cached_git_path = git_path
|
|
return git_path
|
|
|
|
|
|
def _find_git_executable() -> str:
|
|
"""Internal function to find git executable."""
|
|
# 1. Check CLAUDE_CODE_GIT_BASH_PATH (set by Electron frontend)
|
|
# This env var points to bash.exe, we can derive git.exe from it
|
|
bash_path = os.environ.get("CLAUDE_CODE_GIT_BASH_PATH")
|
|
if bash_path:
|
|
try:
|
|
bash_path_obj = Path(bash_path)
|
|
if bash_path_obj.exists():
|
|
git_dir = bash_path_obj.parent.parent
|
|
# Try cmd/git.exe first (preferred), then bin/git.exe
|
|
for git_subpath in ["cmd/git.exe", "bin/git.exe"]:
|
|
git_path = git_dir / git_subpath
|
|
if git_path.is_file():
|
|
return str(git_path)
|
|
except (OSError, ValueError):
|
|
pass # Invalid path or permission error - try next method
|
|
|
|
# 2. Try shutil.which (works if git is in PATH)
|
|
git_path = shutil.which("git")
|
|
if git_path:
|
|
return git_path
|
|
|
|
# 3. Windows-specific: check common installation locations
|
|
if os.name == "nt":
|
|
common_paths = [
|
|
os.path.expandvars(r"%PROGRAMFILES%\Git\cmd\git.exe"),
|
|
os.path.expandvars(r"%PROGRAMFILES%\Git\bin\git.exe"),
|
|
os.path.expandvars(r"%PROGRAMFILES(X86)%\Git\cmd\git.exe"),
|
|
os.path.expandvars(r"%LOCALAPPDATA%\Programs\Git\cmd\git.exe"),
|
|
r"C:\Program Files\Git\cmd\git.exe",
|
|
r"C:\Program Files (x86)\Git\cmd\git.exe",
|
|
]
|
|
for path in common_paths:
|
|
try:
|
|
if os.path.isfile(path):
|
|
return path
|
|
except OSError:
|
|
continue
|
|
|
|
# 4. Try 'where' command with full path (works even when System32 isn't in PATH)
|
|
try:
|
|
result = subprocess.run(
|
|
[get_where_exe_path(), "git"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
)
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
found_path = result.stdout.strip().split("\n")[0].strip()
|
|
if found_path and os.path.isfile(found_path):
|
|
return found_path
|
|
except (subprocess.TimeoutExpired, OSError):
|
|
pass # 'where' command failed - fall through to default
|
|
|
|
# Default fallback - let subprocess handle it (may fail)
|
|
return "git"
|
|
|
|
|
|
def run_git(
|
|
args: list[str],
|
|
cwd: Path | str | None = None,
|
|
timeout: int = 60,
|
|
input_data: str | None = None,
|
|
env: dict | None = None,
|
|
isolate_env: bool = True,
|
|
) -> subprocess.CompletedProcess:
|
|
"""Run a git command with proper executable finding and environment isolation.
|
|
|
|
Args:
|
|
args: Git command arguments (without 'git' prefix)
|
|
cwd: Working directory for the command
|
|
timeout: Command timeout in seconds (default: 60)
|
|
input_data: Optional string data to pass to stdin
|
|
env: Custom environment dict. If None and isolate_env=True, uses isolated env.
|
|
isolate_env: If True (default), clears git env vars to prevent hook interference.
|
|
|
|
Returns:
|
|
CompletedProcess with command results.
|
|
"""
|
|
git = get_git_executable()
|
|
|
|
if env is None and isolate_env:
|
|
env = get_isolated_git_env()
|
|
|
|
try:
|
|
return subprocess.run(
|
|
[git] + args,
|
|
cwd=cwd,
|
|
input=input_data,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
timeout=timeout,
|
|
env=env,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return subprocess.CompletedProcess(
|
|
args=[git] + args,
|
|
returncode=-1,
|
|
stdout="",
|
|
stderr=f"Command timed out after {timeout} seconds",
|
|
)
|
|
except FileNotFoundError:
|
|
return subprocess.CompletedProcess(
|
|
args=[git] + args,
|
|
returncode=-1,
|
|
stdout="",
|
|
stderr="Git executable not found. Please ensure git is installed and in PATH.",
|
|
)
|