fix: detect and clear cross-platform CLI paths in settings (#535)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Andy
2026-01-02 11:30:10 +01:00
committed by GitHub
parent 7990dcb41d
commit 29e455058b
6 changed files with 199 additions and 68 deletions
+1 -1
View File
@@ -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:
+147 -56
View File
@@ -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);
}
@@ -51,7 +51,12 @@ export function registerFileHandlers(): void {
IPC_CHANNELS.FILE_EXPLORER_LIST,
async (_, dirPath: string): Promise<IPCResult<FileNode[]>> => {
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()
});
@@ -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<void> => {
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 {
@@ -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)
+1 -1
View File
@@ -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';