From 29e455058bd5a4a5373bc912fe6b149277ebcef3 Mon Sep 17 00:00:00 2001 From: Andy <119136210+AndyMik90@users.noreply.github.com> Date: Fri, 2 Jan 2026 11:30:10 +0100 Subject: [PATCH] fix: detect and clear cross-platform CLI paths in settings (#535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: fix frontend lag with batched IPC events and optimized store updates Critical performance fixes addressing 2-5s UI lag during task execution: Frontend optimizations: - Batch IPC log events (100+/sec → 6/sec batched updates) - Add batchAppendLogs to task-store for efficient log appending - Only set updatedAt on phase changes, not every progress tick - Memoize sanitizeMarkdownForDisplay and formatRelativeTime in TaskCard - Add React.memo with custom comparators to DroppableColumn - Use IntersectionObserver to pause animations when cards not visible - Reduce debug logging verbosity Backend optimizations: - Add project index caching with 5-minute TTL in client.py - Batch StatusManager file writes with threading.Timer debounce 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * chore(deps): update all frontend dependencies including major versions Updates all frontend dependencies to latest versions: - react-resizable-panels 3.0.6 → 4.2.0 (breaking API change) - globals 16.5.0 → 17.0.0 - lucide-react 0.560.0 → 0.562.0 - zod 4.2.1 → 4.3.4 - Plus other minor/patch updates Updates TerminalGrid.tsx for react-resizable-panels v4 API: - PanelGroup → Group - PanelResizeHandle → Separator - direction → orientation - Removed order prop - Changed div wrappers to React.Fragment for proper resize handling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * fix(memory): fix learning loop to retrieve patterns and gotchas The memory system was storing patterns and gotchas correctly (100% working) but never retrieving them for agent prompts. The root cause was that get_relevant_context() only performed generic semantic search without filtering for specific episode types. Changes: - Add get_patterns_and_gotchas() method to search.py that specifically retrieves PATTERN and GOTCHA episodes with focused queries - Add min_score filtering to reduce noise from low-relevance results - Add wrapper method to graphiti.py facade class - Update memory_manager.py to call new method and format results into dedicated "Learned Patterns" and "Known Gotchas" sections This enables cross-session learning where patterns discovered in session 1 will now be available to sessions 2, 3, 4, etc. * memory is now a app wide setting * fix: detect and clear cross-platform CLI paths in settings When settings are synced/transferred between platforms (e.g., Windows to macOS), CLI tool paths can persist with wrong platform separators causing "Claude Code not found" errors with Windows paths on macOS. Changes: - Add isWrongPlatformPath() to detect paths from different platforms - Update all CLI tool detection methods to skip wrong-platform paths - Add settings migration to clear cross-platform paths on load - Export isPathFromWrongPlatform() for use in settings handlers Fixes issue where Windows paths like C:\Users\...\claude.exe appeared in error messages on macOS systems. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * fix(security): address CodeRabbit security findings Security fixes: - [CRITICAL] Fix command injection in validatePython() by using execFileSync instead of execSync with string interpolation (cli-tool-manager.ts) - [HIGH] Add path validation to FILE_EXPLORER_LIST to prevent directory traversal attacks (file-handlers.ts) - [HIGH] Fix xterm shell injection by using cwd option instead of embedding path in bash -c command (settings-handlers.ts) - [MEDIUM] Add URL scheme validation to SHELL_OPEN_EXTERNAL to block dangerous protocols like file:// and javascript: (settings-handlers.ts) Other fixes: - [MEDIUM] Fix TaskCard memo comparison to check all subtasks, not just first 5 (TaskCard.tsx) - [LOW] Fix type hint for optional BuildStatus parameter (status.py) - [LOW] Remove unused useRef import (useIpc.ts) Already fixed (no action needed): - Race conditions in client.py and status.py (locks already in place) - IntersectionObserver in PhaseProgressIndicator (dependency array already []) - Unused imports in KanbanBoard.tsx (already removed) - memory-env-builder.ts exists (CodeRabbit incorrectly reported missing) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- apps/backend/ui/status.py | 2 +- apps/frontend/src/main/cli-tool-manager.ts | 203 +++++++++++++----- .../src/main/ipc-handlers/file-handlers.ts | 9 +- .../main/ipc-handlers/settings-handlers.ts | 47 +++- .../src/renderer/components/TaskCard.tsx | 4 +- apps/frontend/src/renderer/hooks/useIpc.ts | 2 +- 6 files changed, 199 insertions(+), 68 deletions(-) diff --git a/apps/backend/ui/status.py b/apps/backend/ui/status.py index b4927c46..d176280f 100644 --- a/apps/backend/ui/status.py +++ b/apps/backend/ui/status.py @@ -182,7 +182,7 @@ class StatusManager: f"[StatusManager] Scheduled batched write in {self._WRITE_DEBOUNCE_MS}ms" ) - def write(self, status: BuildStatus = None, immediate: bool = False) -> None: + def write(self, status: BuildStatus | None = None, immediate: bool = False) -> None: """Write status to file. Args: diff --git a/apps/frontend/src/main/cli-tool-manager.ts b/apps/frontend/src/main/cli-tool-manager.ts index 4e0c6d0a..a92bbc96 100644 --- a/apps/frontend/src/main/cli-tool-manager.ts +++ b/apps/frontend/src/main/cli-tool-manager.ts @@ -20,7 +20,7 @@ * - Graceful fallbacks when tools not found */ -import { execSync, execFileSync } from 'child_process'; +import { execFileSync } from 'child_process'; import { existsSync } from 'fs'; import path from 'path'; import os from 'os'; @@ -64,6 +64,45 @@ interface CacheEntry { source: string; } +/** + * Check if a path appears to be from a different platform. + * Detects Windows paths on Unix and Unix paths on Windows. + * + * @param pathStr - The path to check + * @returns true if the path is from a different platform + */ +function isWrongPlatformPath(pathStr: string | undefined): boolean { + if (!pathStr) return false; + + const isWindows = process.platform === 'win32'; + + if (isWindows) { + // On Windows, reject Unix-style absolute paths (starting with /) + // but allow relative paths and Windows paths + if (pathStr.startsWith('/') && !pathStr.startsWith('//')) { + // Unix absolute path on Windows + return true; + } + } else { + // On Unix (macOS/Linux), reject Windows-style paths + // Windows paths have: drive letter (C:), backslashes, or specific Windows paths + if (/^[A-Za-z]:[/\\]/.test(pathStr)) { + // Drive letter path (C:\, D:/, etc.) + return true; + } + if (pathStr.includes('\\')) { + // Contains backslashes (Windows path separators) + return true; + } + if (pathStr.includes('AppData') || pathStr.includes('Program Files')) { + // Contains Windows-specific directory names + return true; + } + } + + return false; +} + /** * Centralized CLI Tool Manager * @@ -165,7 +204,7 @@ class CLIToolManager { * Detect Python with multi-level priority * * Priority order: - * 1. User configuration + * 1. User configuration (if valid for current platform) * 2. Bundled Python (packaged apps only) * 3. Homebrew Python (macOS) * 4. System PATH (py -3, python3, python) @@ -179,19 +218,26 @@ class CLIToolManager { // 1. User configuration if (this.userConfig.pythonPath) { - const validation = this.validatePython(this.userConfig.pythonPath); - if (validation.valid) { - return { - found: true, - path: this.userConfig.pythonPath, - version: validation.version, - source: 'user-config', - message: `Using user-configured Python: ${this.userConfig.pythonPath}`, - }; + // Check if path is from wrong platform (e.g., Windows path on macOS) + if (isWrongPlatformPath(this.userConfig.pythonPath)) { + console.warn( + `[Python] User-configured path is from different platform, ignoring: ${this.userConfig.pythonPath}` + ); + } else { + const validation = this.validatePython(this.userConfig.pythonPath); + if (validation.valid) { + return { + found: true, + path: this.userConfig.pythonPath, + version: validation.version, + source: 'user-config', + message: `Using user-configured Python: ${this.userConfig.pythonPath}`, + }; + } + console.warn( + `[Python] User-configured path invalid: ${validation.message}` + ); } - console.warn( - `[Python] User-configured path invalid: ${validation.message}` - ); } // 2. Bundled Python (packaged apps only) @@ -279,7 +325,7 @@ class CLIToolManager { * Detect Git with multi-level priority * * Priority order: - * 1. User configuration + * 1. User configuration (if valid for current platform) * 2. Homebrew Git (macOS) * 3. System PATH * @@ -288,17 +334,24 @@ class CLIToolManager { private detectGit(): ToolDetectionResult { // 1. User configuration if (this.userConfig.gitPath) { - const validation = this.validateGit(this.userConfig.gitPath); - if (validation.valid) { - return { - found: true, - path: this.userConfig.gitPath, - version: validation.version, - source: 'user-config', - message: `Using user-configured Git: ${this.userConfig.gitPath}`, - }; + // Check if path is from wrong platform (e.g., Windows path on macOS) + if (isWrongPlatformPath(this.userConfig.gitPath)) { + console.warn( + `[Git] User-configured path is from different platform, ignoring: ${this.userConfig.gitPath}` + ); + } else { + const validation = this.validateGit(this.userConfig.gitPath); + if (validation.valid) { + return { + found: true, + path: this.userConfig.gitPath, + version: validation.version, + source: 'user-config', + message: `Using user-configured Git: ${this.userConfig.gitPath}`, + }; + } + console.warn(`[Git] User-configured path invalid: ${validation.message}`); } - console.warn(`[Git] User-configured path invalid: ${validation.message}`); } // 2. Homebrew (macOS) @@ -351,7 +404,7 @@ class CLIToolManager { * Detect GitHub CLI with multi-level priority * * Priority order: - * 1. User configuration + * 1. User configuration (if valid for current platform) * 2. Homebrew gh (macOS) * 3. System PATH * 4. Windows Program Files @@ -361,19 +414,26 @@ class CLIToolManager { private detectGitHubCLI(): ToolDetectionResult { // 1. User configuration if (this.userConfig.githubCLIPath) { - const validation = this.validateGitHubCLI(this.userConfig.githubCLIPath); - if (validation.valid) { - return { - found: true, - path: this.userConfig.githubCLIPath, - version: validation.version, - source: 'user-config', - message: `Using user-configured GitHub CLI: ${this.userConfig.githubCLIPath}`, - }; + // Check if path is from wrong platform (e.g., Windows path on macOS) + if (isWrongPlatformPath(this.userConfig.githubCLIPath)) { + console.warn( + `[GitHub CLI] User-configured path is from different platform, ignoring: ${this.userConfig.githubCLIPath}` + ); + } else { + const validation = this.validateGitHubCLI(this.userConfig.githubCLIPath); + if (validation.valid) { + return { + found: true, + path: this.userConfig.githubCLIPath, + version: validation.version, + source: 'user-config', + message: `Using user-configured GitHub CLI: ${this.userConfig.githubCLIPath}`, + }; + } + console.warn( + `[GitHub CLI] User-configured path invalid: ${validation.message}` + ); } - console.warn( - `[GitHub CLI] User-configured path invalid: ${validation.message}` - ); } // 2. Homebrew (macOS) @@ -449,7 +509,7 @@ class CLIToolManager { * Detect Claude CLI with multi-level priority * * Priority order: - * 1. User configuration + * 1. User configuration (if valid for current platform) * 2. Homebrew claude (macOS) * 3. System PATH * 4. Windows/macOS/Linux standard locations @@ -459,19 +519,26 @@ class CLIToolManager { private detectClaude(): ToolDetectionResult { // 1. User configuration if (this.userConfig.claudePath) { - const validation = this.validateClaude(this.userConfig.claudePath); - if (validation.valid) { - return { - found: true, - path: this.userConfig.claudePath, - version: validation.version, - source: 'user-config', - message: `Using user-configured Claude CLI: ${this.userConfig.claudePath}`, - }; + // Check if path is from wrong platform (e.g., Windows path on macOS) + if (isWrongPlatformPath(this.userConfig.claudePath)) { + console.warn( + `[Claude CLI] User-configured path is from different platform, ignoring: ${this.userConfig.claudePath}` + ); + } else { + const validation = this.validateClaude(this.userConfig.claudePath); + if (validation.valid) { + return { + found: true, + path: this.userConfig.claudePath, + version: validation.version, + source: 'user-config', + message: `Using user-configured Claude CLI: ${this.userConfig.claudePath}`, + }; + } + console.warn( + `[Claude CLI] User-configured path invalid: ${validation.message}` + ); } - console.warn( - `[Claude CLI] User-configured path invalid: ${validation.message}` - ); } // 2. Homebrew (macOS) @@ -563,13 +630,17 @@ class CLIToolManager { const MINIMUM_VERSION = '3.10.0'; try { - const version = execSync(`${pythonCmd} --version`, { - stdio: 'pipe', + // Parse command to handle cases like 'py -3' on Windows + // This avoids command injection by using execFileSync instead of execSync + const parts = pythonCmd.split(' '); + const cmd = parts[0]; + const args = [...parts.slice(1), '--version']; + + const version = execFileSync(cmd, args, { + encoding: 'utf-8', timeout: 5000, windowsHide: true, - }) - .toString() - .trim(); + }).trim(); const match = version.match(/Python (\d+\.\d+\.\d+)/); if (!match) { @@ -860,3 +931,23 @@ export function getToolInfo(tool: CLITool): ToolDetectionResult { export function clearToolCache(): void { cliToolManager.clearCache(); } + +/** + * Check if a path appears to be from a different platform. + * Useful for detecting cross-platform path issues in settings. + * + * @param pathStr - The path to check + * @returns true if the path is from a different platform + * + * @example + * ```typescript + * import { isPathFromWrongPlatform } from './cli-tool-manager'; + * + * // On macOS, this returns true for Windows paths + * isPathFromWrongPlatform('C:\\Program Files\\claude.exe'); // true + * isPathFromWrongPlatform('/usr/local/bin/claude'); // false + * ``` + */ +export function isPathFromWrongPlatform(pathStr: string | undefined): boolean { + return isWrongPlatformPath(pathStr); +} diff --git a/apps/frontend/src/main/ipc-handlers/file-handlers.ts b/apps/frontend/src/main/ipc-handlers/file-handlers.ts index fc32e2f0..5ffb952b 100644 --- a/apps/frontend/src/main/ipc-handlers/file-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/file-handlers.ts @@ -51,7 +51,12 @@ export function registerFileHandlers(): void { IPC_CHANNELS.FILE_EXPLORER_LIST, async (_, dirPath: string): Promise> => { try { - const entries = readdirSync(dirPath, { withFileTypes: true }); + // Validate and normalize path to prevent directory traversal + const validation = validatePath(dirPath); + if (!validation.valid) { + return { success: false, error: validation.error }; + } + const entries = readdirSync(validation.path, { withFileTypes: true }); // Filter and map entries const nodes: FileNode[] = []; @@ -65,7 +70,7 @@ export function registerFileHandlers(): void { if (entry.isDirectory() && IGNORED_DIRS.has(entry.name)) continue; nodes.push({ - path: path.join(dirPath, entry.name), + path: path.join(validation.path, entry.name), name: entry.name, isDirectory: entry.isDirectory() }); diff --git a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts index 8a49d843..d6e7b94f 100644 --- a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts @@ -13,7 +13,7 @@ import type { BrowserWindow } from 'electron'; import { getEffectiveVersion } from '../auto-claude-updater'; import { setUpdateChannel } from '../app-updater'; import { getSettingsPath, readSettingsFile } from '../settings-utils'; -import { configureTools, getToolPath, getToolInfo } from '../cli-tool-manager'; +import { configureTools, getToolPath, getToolInfo, isPathFromWrongPlatform } from '../cli-tool-manager'; const settingsPath = getSettingsPath(); @@ -124,6 +124,22 @@ export function registerSettingsHandlers( needsSave = true; } + // Migration: Clear CLI tool paths that are from a different platform + // Fixes issue where Windows paths persisted on macOS (and vice versa) + // when settings were synced/transferred between platforms + // See: https://github.com/AndyMik90/Auto-Claude/issues/XXX + const pathFields = ['pythonPath', 'gitPath', 'githubCLIPath', 'claudePath', 'autoBuildPath'] as const; + for (const field of pathFields) { + const pathValue = settings[field]; + if (pathValue && isPathFromWrongPlatform(pathValue)) { + console.warn( + `[SETTINGS_GET] Clearing ${field} - path from different platform: ${pathValue}` + ); + delete settings[field]; + needsSave = true; + } + } + // If no manual autoBuildPath is set, try to auto-detect if (!settings.autoBuildPath) { const detectedPath = detectAutoBuildSourcePath(); @@ -369,7 +385,22 @@ export function registerSettingsHandlers( ipcMain.handle( IPC_CHANNELS.SHELL_OPEN_EXTERNAL, async (_, url: string): Promise => { - await shell.openExternal(url); + // Validate URL scheme to prevent opening dangerous protocols + try { + const parsedUrl = new URL(url); + if (!['http:', 'https:'].includes(parsedUrl.protocol)) { + console.warn(`[SHELL_OPEN_EXTERNAL] Blocked URL with unsafe protocol: ${parsedUrl.protocol}`); + throw new Error(`Unsafe URL protocol: ${parsedUrl.protocol}`); + } + await shell.openExternal(url); + } catch (error) { + if (error instanceof TypeError) { + // Invalid URL format + console.warn(`[SHELL_OPEN_EXTERNAL] Invalid URL format: ${url}`); + throw new Error('Invalid URL format'); + } + throw error; + } } ); @@ -427,17 +458,21 @@ export function registerSettingsHandlers( }); } else { // Linux: Try common terminal emulators with argument arrays - const terminals: Array<{ cmd: string; args: string[] }> = [ + // Note: xterm uses cwd option to avoid shell injection vulnerabilities + const terminals: Array<{ cmd: string; args: string[]; useCwd?: boolean }> = [ { cmd: 'gnome-terminal', args: ['--working-directory', resolvedPath] }, { cmd: 'konsole', args: ['--workdir', resolvedPath] }, { cmd: 'xfce4-terminal', args: ['--working-directory', resolvedPath] }, - { cmd: 'xterm', args: ['-e', 'bash', '-c', `cd '${resolvedPath.replace(/'/g, "'\\''")}' && exec bash`] } + { cmd: 'xterm', args: ['-e', 'bash'], useCwd: true } ]; let opened = false; - for (const { cmd, args } of terminals) { + for (const { cmd, args, useCwd } of terminals) { try { - execFileSync(cmd, args, { stdio: 'ignore' }); + execFileSync(cmd, args, { + stdio: 'ignore', + ...(useCwd ? { cwd: resolvedPath } : {}) + }); opened = true; break; } catch { diff --git a/apps/frontend/src/renderer/components/TaskCard.tsx b/apps/frontend/src/renderer/components/TaskCard.tsx index ecbc7c67..87ee9751 100644 --- a/apps/frontend/src/renderer/components/TaskCard.tsx +++ b/apps/frontend/src/renderer/components/TaskCard.tsx @@ -63,8 +63,8 @@ function taskCardPropsAreEqual(prevProps: TaskCardProps, nextProps: TaskCardProp prevTask.metadata?.category === nextTask.metadata?.category && prevTask.metadata?.complexity === nextTask.metadata?.complexity && prevTask.metadata?.archivedAt === nextTask.metadata?.archivedAt && - // Check if subtask statuses changed (quick check on first few) - prevTask.subtasks.slice(0, 5).every((s, i) => s.status === nextTask.subtasks[i]?.status) + // Check if any subtask statuses changed (compare all subtasks) + prevTask.subtasks.every((s, i) => s.status === nextTask.subtasks[i]?.status) ); // Only log when actually re-rendering (reduces noise significantly) diff --git a/apps/frontend/src/renderer/hooks/useIpc.ts b/apps/frontend/src/renderer/hooks/useIpc.ts index 10404e23..7e0f2f5e 100644 --- a/apps/frontend/src/renderer/hooks/useIpc.ts +++ b/apps/frontend/src/renderer/hooks/useIpc.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { unstable_batchedUpdates } from 'react-dom'; import { useTaskStore } from '../stores/task-store'; import { useRoadmapStore } from '../stores/roadmap-store';