From 33d736d60b0755167cb0ec1bc6c77ad4edc7c77e Mon Sep 17 00:00:00 2001 From: Scott Beardsley Date: Wed, 31 Dec 2025 09:16:36 -0500 Subject: [PATCH 1/6] feat: add UI configuration for custom worktree location Allows users to configure worktree location per project through the project settings UI. The custom path is stored in the project's .env file as WORKTREE_BASE_PATH. Changes: - Add worktreePath field to ProjectEnvConfig type - Create WorktreeSettings UI component with path input and browser - Integrate WorktreeSettings into GeneralSettings page - Update env-handlers to read/write WORKTREE_BASE_PATH - Replace all hardcoded .worktrees references with env var Backend files updated: - cli/utils.py - Use WORKTREE_BASE_PATH for spec finding - cli/batch_commands.py - Use env var for cleanup operations - core/workspace/git_utils.py - Use env var for worktree lookups - core/workspace/models.py - Use env var for spec number scanning Frontend files updated: - project-store.ts - Use env var for task scanning - WorktreeSettings.tsx - New component for UI configuration - GeneralSettings.tsx - Integrate worktree settings section - env-handlers.ts - Handle WORKTREE_BASE_PATH in .env file Features: - Supports both relative and absolute paths - Shows resolved path preview - Converts paths intelligently (relative if inside project) - Provides examples and help text - Defaults to .worktrees when not configured --- apps/backend/cli/batch_commands.py | 6 +- apps/backend/cli/utils.py | 3 +- apps/backend/core/workspace/git_utils.py | 6 +- apps/backend/core/workspace/models.py | 4 +- .../src/main/ipc-handlers/env-handlers.ts | 11 +++ apps/frontend/src/main/project-store.ts | 3 +- .../project-settings/GeneralSettings.tsx | 22 ++++- .../project-settings/WorktreeSettings.tsx | 99 +++++++++++++++++++ .../settings/sections/SectionRouter.tsx | 2 + apps/frontend/src/shared/types/project.ts | 1 + 10 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx diff --git a/apps/backend/cli/batch_commands.py b/apps/backend/cli/batch_commands.py index 28a82ea90a..3c0b0ed202 100644 --- a/apps/backend/cli/batch_commands.py +++ b/apps/backend/cli/batch_commands.py @@ -6,6 +6,7 @@ """ import json +import os from pathlib import Path from ui import highlight, print_status @@ -184,7 +185,8 @@ def handle_batch_cleanup_command(project_dir: str, dry_run: bool = True) -> bool True if successful """ specs_dir = Path(project_dir) / ".auto-claude" / "specs" - worktrees_dir = Path(project_dir) / ".worktrees" + worktree_base_path = os.getenv("WORKTREE_BASE_PATH", ".worktrees") + worktrees_dir = Path(project_dir) / worktree_base_path if not specs_dir.exists(): print_status("No specs directory found", "info") @@ -209,7 +211,7 @@ def handle_batch_cleanup_command(project_dir: str, dry_run: bool = True) -> bool print(f" - {spec_name}") wt_path = worktrees_dir / spec_name if wt_path.exists(): - print(f" └─ .worktrees/{spec_name}/") + print(f" └─ {worktree_base_path}/{spec_name}/") print() print("Run with --no-dry-run to actually delete") diff --git a/apps/backend/cli/utils.py b/apps/backend/cli/utils.py index f18954654a..84eddc0fa0 100644 --- a/apps/backend/cli/utils.py +++ b/apps/backend/cli/utils.py @@ -82,7 +82,8 @@ def find_spec(project_dir: Path, spec_identifier: str) -> Path | None: return spec_folder # Check worktree specs (for merge-preview, merge, review, discard operations) - worktree_base = project_dir / ".worktrees" + worktree_base_path = os.getenv("WORKTREE_BASE_PATH", ".worktrees") + worktree_base = project_dir / worktree_base_path if worktree_base.exists(): # Try exact match in worktree worktree_spec = ( diff --git a/apps/backend/core/workspace/git_utils.py b/apps/backend/core/workspace/git_utils.py index d460dc9728..fc43c62c7f 100644 --- a/apps/backend/core/workspace/git_utils.py +++ b/apps/backend/core/workspace/git_utils.py @@ -7,6 +7,7 @@ """ import json +import os import subprocess from pathlib import Path @@ -221,8 +222,9 @@ def get_existing_build_worktree(project_dir: Path, spec_name: str) -> Path | Non Returns: Path to the worktree if it exists for this spec, None otherwise """ - # Per-spec worktree path: .worktrees/{spec-name}/ - worktree_path = project_dir / ".worktrees" / spec_name + # Per-spec worktree path: .worktrees/{spec-name}/ (or custom WORKTREE_BASE_PATH) + worktree_base_path = os.getenv("WORKTREE_BASE_PATH", ".worktrees") + worktree_path = project_dir / worktree_base_path / spec_name if worktree_path.exists(): return worktree_path return None diff --git a/apps/backend/core/workspace/models.py b/apps/backend/core/workspace/models.py index cc94413e54..c64c01e748 100644 --- a/apps/backend/core/workspace/models.py +++ b/apps/backend/core/workspace/models.py @@ -6,6 +6,7 @@ Data classes and enums for workspace management. """ +import os from dataclasses import dataclass from enum import Enum from pathlib import Path @@ -249,7 +250,8 @@ def get_next_spec_number(self) -> int: max_number = max(max_number, self._scan_specs_dir(main_specs_dir)) # 2. Scan all worktree specs - worktrees_dir = self.project_dir / ".worktrees" + worktree_base_path = os.getenv("WORKTREE_BASE_PATH", ".worktrees") + worktrees_dir = self.project_dir / worktree_base_path if worktrees_dir.exists(): for worktree in worktrees_dir.iterdir(): if worktree.is_dir(): diff --git a/apps/frontend/src/main/ipc-handlers/env-handlers.ts b/apps/frontend/src/main/ipc-handlers/env-handlers.ts index 661d29997b..6970c6f5e1 100644 --- a/apps/frontend/src/main/ipc-handlers/env-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/env-handlers.ts @@ -98,6 +98,9 @@ export function registerEnvHandlers( if (config.defaultBranch !== undefined) { existingVars['DEFAULT_BRANCH'] = config.defaultBranch; } + if (config.worktreePath !== undefined) { + existingVars['WORKTREE_BASE_PATH'] = config.worktreePath; + } if (config.graphitiEnabled !== undefined) { existingVars['GRAPHITI_ENABLED'] = config.graphitiEnabled ? 'true' : 'false'; } @@ -182,6 +185,11 @@ ${envLine(existingVars, GITLAB_ENV_KEYS.AUTO_SYNC, 'false')} # If not set, Auto Claude will auto-detect main/master, or fall back to current branch ${existingVars['DEFAULT_BRANCH'] ? `DEFAULT_BRANCH=${existingVars['DEFAULT_BRANCH']}` : '# DEFAULT_BRANCH=main'} +# Worktree base path (OPTIONAL) +# Supports relative paths (e.g., worktrees) or absolute paths (e.g., /tmp/worktrees) +# If not set, defaults to .worktrees in your project root +${existingVars['WORKTREE_BASE_PATH'] ? `WORKTREE_BASE_PATH=${existingVars['WORKTREE_BASE_PATH']}` : '# WORKTREE_BASE_PATH=.worktrees'} + # ============================================================================= # UI SETTINGS (OPTIONAL) # ============================================================================= @@ -335,6 +343,9 @@ ${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_ if (vars['DEFAULT_BRANCH']) { config.defaultBranch = vars['DEFAULT_BRANCH']; } + if (vars['WORKTREE_BASE_PATH']) { + config.worktreePath = vars['WORKTREE_BASE_PATH']; + } if (vars['GRAPHITI_ENABLED']?.toLowerCase() === 'true') { config.graphitiEnabled = true; diff --git a/apps/frontend/src/main/project-store.ts b/apps/frontend/src/main/project-store.ts index be1bf529a5..48676b41c0 100644 --- a/apps/frontend/src/main/project-store.ts +++ b/apps/frontend/src/main/project-store.ts @@ -264,7 +264,8 @@ export class ProjectStore { // NOTE FOR MAINTAINERS: Worktree tasks are only included if the spec also exists in main. // This prevents deleted tasks from "coming back" when the worktree isn't cleaned up. // Alternative behavior: include all worktree tasks (remove the mainSpecIds check below). - const worktreesDir = path.join(project.path, '.worktrees'); + const worktreeBasePath = process.env.WORKTREE_BASE_PATH || '.worktrees'; + const worktreesDir = path.join(project.path, worktreeBasePath); if (existsSync(worktreesDir)) { try { const worktrees = readdirSync(worktreesDir, { withFileTypes: true }); diff --git a/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx b/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx index 7899f6dbb8..63e3253e52 100644 --- a/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx +++ b/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx @@ -21,8 +21,10 @@ import { AVAILABLE_MODELS } from '../../../shared/constants'; import type { Project, ProjectSettings as ProjectSettingsType, - AutoBuildVersionInfo + AutoBuildVersionInfo, + ProjectEnvConfig } from '../../../shared/types'; +import { WorktreeSettings } from './WorktreeSettings'; interface GeneralSettingsProps { project: Project; @@ -32,6 +34,8 @@ interface GeneralSettingsProps { isCheckingVersion: boolean; isUpdating: boolean; handleInitialize: () => Promise; + envConfig: ProjectEnvConfig | null; + updateEnvConfig: (updates: Partial) => void; } export function GeneralSettings({ @@ -41,7 +45,9 @@ export function GeneralSettings({ versionInfo, isCheckingVersion, isUpdating, - handleInitialize + handleInitialize, + envConfig, + updateEnvConfig }: GeneralSettingsProps) { const { t } = useTranslation(['settings']); @@ -150,6 +156,18 @@ export function GeneralSettings({ + {/* Worktree Location */} +
+

Worktree Location

+ +
+ + + {/* Notifications */}

Notifications

diff --git a/apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx b/apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx new file mode 100644 index 0000000000..df02262e06 --- /dev/null +++ b/apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react'; +import { FolderOpen, Info } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Label } from '../ui/label'; +import { Input } from '../ui/input'; +import { Button } from '../ui/button'; +import type { ProjectEnvConfig } from '../../../shared/types'; + +const path = window.require('path'); + +interface WorktreeSettingsProps { + envConfig: ProjectEnvConfig | null; + updateEnvConfig: (updates: Partial) => void; + projectPath: string; +} + +export function WorktreeSettings({ + envConfig, + updateEnvConfig, + projectPath +}: WorktreeSettingsProps) { + const { t } = useTranslation(['settings']); + const worktreePath = envConfig?.worktreePath || ''; + + // Resolve the actual path that will be used + const resolvedPath = worktreePath + ? (path.isAbsolute(worktreePath) + ? worktreePath + : path.join(projectPath, worktreePath)) + : path.join(projectPath, '.worktrees'); + + const handleBrowse = async () => { + const result = await window.electronAPI.dialog.showOpenDialog({ + properties: ['openDirectory', 'createDirectory'], + defaultPath: projectPath, + title: 'Select Worktree Location' + }); + + if (!result.canceled && result.filePaths.length > 0) { + const selectedPath = result.filePaths[0]; + // Convert to relative path if inside project + const relativePath = path.relative(projectPath, selectedPath); + const finalPath = relativePath.startsWith('..') + ? selectedPath // Absolute if outside project + : relativePath; // Relative if inside project + + updateEnvConfig({ worktreePath: finalPath }); + } + }; + + return ( +
+
+ +
+

Worktrees are isolated Git branches where Auto Claude builds features safely.

+

Default: .worktrees/ in your project root

+
+
+ +
+ +
+ updateEnvConfig({ worktreePath: e.target.value })} + /> + +
+

+ Supports relative paths (e.g., worktrees) or absolute paths (e.g., /tmp/worktrees) +

+
+ +
+

Resolved Path:

+ {resolvedPath} +
+ +
+

Common Use Cases:

+
    +
  • External drive: /Volumes/FastSSD/worktrees
  • +
  • Temp directory: /tmp/my-project-worktrees
  • +
  • Shared builds: ../shared-worktrees
  • +
+
+
+ ); +} diff --git a/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx b/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx index 663f6d2700..7d1a969a89 100644 --- a/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx +++ b/apps/frontend/src/renderer/components/settings/sections/SectionRouter.tsx @@ -100,6 +100,8 @@ export function SectionRouter({ isCheckingVersion={isCheckingVersion} isUpdating={isUpdating} handleInitialize={handleInitialize} + envConfig={envConfig} + updateEnvConfig={updateEnvConfig} /> ); diff --git a/apps/frontend/src/shared/types/project.ts b/apps/frontend/src/shared/types/project.ts index fd34224be2..38510cb661 100644 --- a/apps/frontend/src/shared/types/project.ts +++ b/apps/frontend/src/shared/types/project.ts @@ -304,6 +304,7 @@ export interface ProjectEnvConfig { // Git/Worktree Settings defaultBranch?: string; // Base branch for worktree creation (e.g., 'main', 'develop') + worktreePath?: string; // Custom worktree base path (relative or absolute) // Graphiti Memory Integration (V2 - Multi-provider support) // Uses LadybugDB embedded database (no Docker required, Python 3.12+) From d19b1596e3c3216fc7d980e60915296afbcb64a2 Mon Sep 17 00:00:00 2001 From: Scott Beardsley Date: Wed, 31 Dec 2025 09:24:54 -0500 Subject: [PATCH 2/6] fix: use browser-compatible path utilities in WorktreeSettings Replace window.require('path') with custom browser-compatible path utilities to avoid 'window.require is not a function' error in the Electron renderer process. - Add isAbsolutePath() to detect absolute paths (Unix/Windows/UNC) - Add joinPath() for simple path joining - Add getRelativePath() to convert absolute to relative paths - All functions work in browser context without Node.js modules --- .../project-settings/WorktreeSettings.tsx | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx b/apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx index df02262e06..196e3b0c2a 100644 --- a/apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx +++ b/apps/frontend/src/renderer/components/project-settings/WorktreeSettings.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { FolderOpen, Info } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Label } from '../ui/label'; @@ -6,7 +5,50 @@ import { Input } from '../ui/input'; import { Button } from '../ui/button'; import type { ProjectEnvConfig } from '../../../shared/types'; -const path = window.require('path'); +// Browser-compatible path utilities +const isAbsolutePath = (p: string): boolean => { + // Unix absolute path starts with / + // Windows absolute path starts with drive letter (C:\) or UNC path (\\) + return p.startsWith('/') || /^[a-zA-Z]:[/\\]/.test(p) || p.startsWith('\\\\'); +}; + +const joinPath = (...parts: string[]): string => { + // Simple path join that works in browser + return parts.join('/').replace(/\/+/g, '/'); +}; + +const getRelativePath = (from: string, to: string): string => { + // Normalize paths + const fromParts = from.split(/[/\\]/).filter(Boolean); + const toParts = to.split(/[/\\]/).filter(Boolean); + + // Find common base + let commonLength = 0; + const minLength = Math.min(fromParts.length, toParts.length); + for (let i = 0; i < minLength; i++) { + if (fromParts[i] === toParts[i]) { + commonLength = i + 1; + } else { + break; + } + } + + // If no common base, paths are on different roots + if (commonLength === 0) { + return to; // Return absolute path + } + + // Build relative path + const upCount = fromParts.length - commonLength; + const downParts = toParts.slice(commonLength); + + if (upCount === 0 && downParts.length === 0) { + return '.'; + } + + const ups = Array(upCount).fill('..'); + return [...ups, ...downParts].join('/'); +}; interface WorktreeSettingsProps { envConfig: ProjectEnvConfig | null; @@ -24,10 +66,10 @@ export function WorktreeSettings({ // Resolve the actual path that will be used const resolvedPath = worktreePath - ? (path.isAbsolute(worktreePath) + ? (isAbsolutePath(worktreePath) ? worktreePath - : path.join(projectPath, worktreePath)) - : path.join(projectPath, '.worktrees'); + : joinPath(projectPath, worktreePath)) + : joinPath(projectPath, '.worktrees'); const handleBrowse = async () => { const result = await window.electronAPI.dialog.showOpenDialog({ @@ -39,7 +81,7 @@ export function WorktreeSettings({ if (!result.canceled && result.filePaths.length > 0) { const selectedPath = result.filePaths[0]; // Convert to relative path if inside project - const relativePath = path.relative(projectPath, selectedPath); + const relativePath = getRelativePath(projectPath, selectedPath); const finalPath = relativePath.startsWith('..') ? selectedPath // Absolute if outside project : relativePath; // Relative if inside project From 87f2aa8ef25c7ffbd88be45e73b381e05c6a6d9d Mon Sep 17 00:00:00 2001 From: Scott Beardsley Date: Wed, 31 Dec 2025 10:02:46 -0500 Subject: [PATCH 3/6] fix: load project .env file and use WORKTREE_BASE_PATH in WorktreeManager Critical fixes to make worktree path configuration actually work: 1. WorktreeManager now reads WORKTREE_BASE_PATH environment variable - Previously hardcoded to .worktrees - Now respects custom path from project settings 2. Load project's .env file after determining project directory - Backend was only loading apps/backend/.env - Now also loads /.auto-claude/.env with override=True - This ensures WORKTREE_BASE_PATH is available to all code Without these changes, the UI setting was being saved but never used. Files modified: - apps/backend/core/worktree.py - Use WORKTREE_BASE_PATH env var - apps/backend/cli/main.py - Load project .env file --- apps/backend/cli/main.py | 8 +++ apps/backend/core/worktree.py | 6 +- apps/frontend/package-lock.json | 120 ++++++-------------------------- 3 files changed, 33 insertions(+), 101 deletions(-) diff --git a/apps/backend/cli/main.py b/apps/backend/cli/main.py index 9b910b5311..fc29001456 100644 --- a/apps/backend/cli/main.py +++ b/apps/backend/cli/main.py @@ -15,6 +15,8 @@ if str(_PARENT_DIR) not in sys.path: sys.path.insert(0, str(_PARENT_DIR)) +from dotenv import load_dotenv + from .batch_commands import ( handle_batch_cleanup_command, @@ -276,6 +278,12 @@ def main() -> None: project_dir = get_project_dir(args.project_dir) debug("run.py", f"Using project directory: {project_dir}") + # Load project-specific .env file (overrides backend .env) + project_env = project_dir / ".auto-claude" / ".env" + if project_env.exists(): + load_dotenv(project_env, override=True) + debug("run.py", f"Loaded project .env from: {project_env}") + # Get model from CLI arg or env var (None if not explicitly set) # This allows get_phase_model() to fall back to task_metadata.json model = args.model or os.environ.get("AUTO_BUILD_MODEL") diff --git a/apps/backend/core/worktree.py b/apps/backend/core/worktree.py index ab3b89e3b3..890bde5ddf 100644 --- a/apps/backend/core/worktree.py +++ b/apps/backend/core/worktree.py @@ -55,7 +55,11 @@ class WorktreeManager: def __init__(self, project_dir: Path, base_branch: str | None = None): self.project_dir = project_dir self.base_branch = base_branch or self._detect_base_branch() - self.worktrees_dir = project_dir / ".worktrees" + + # Use custom worktree path from environment variable, default to .worktrees + worktree_base_path = os.getenv("WORKTREE_BASE_PATH", ".worktrees") + self.worktrees_dir = project_dir / worktree_base_path + self._merge_lock = asyncio.Lock() def _detect_base_branch(self) -> str: diff --git a/apps/frontend/package-lock.json b/apps/frontend/package-lock.json index 97bda6b277..d8098fe9da 100644 --- a/apps/frontend/package-lock.json +++ b/apps/frontend/package-lock.json @@ -198,7 +198,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -583,7 +582,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -627,7 +625,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -667,7 +664,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1067,43 +1063,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@epic-web/invariant": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", @@ -4224,7 +4183,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4411,7 +4371,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4422,7 +4381,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4514,7 +4472,6 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -4927,7 +4884,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4988,7 +4944,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5161,6 +5116,7 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -5555,7 +5511,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6220,14 +6175,6 @@ "buffer": "^5.1.0" } }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -6594,7 +6541,6 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -6652,7 +6598,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dotenv": { "version": "16.6.1", @@ -6728,7 +6675,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", @@ -6866,6 +6812,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -6886,6 +6833,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -6901,6 +6849,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -6911,6 +6860,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -7280,7 +7230,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8526,7 +8475,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -9317,7 +9265,6 @@ "integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -10249,6 +10196,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -12439,7 +12387,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12537,7 +12484,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12567,34 +12513,6 @@ "dev": true, "license": "MIT" }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -12611,6 +12529,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -12626,6 +12545,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -12638,7 +12558,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/proc-log": { "version": "2.0.1", @@ -12742,7 +12663,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -12752,7 +12672,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14072,8 +13991,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -14130,6 +14048,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -14156,6 +14075,7 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -14177,6 +14097,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -14190,6 +14111,7 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -14204,6 +14126,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -14520,7 +14443,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14870,7 +14792,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -15912,7 +15833,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 4f35d76291c5fbe436a466c153cf14a5d2c57f13 Mon Sep 17 00:00:00 2001 From: Scott Beardsley Date: Wed, 31 Dec 2025 14:13:07 -0500 Subject: [PATCH 4/6] refactor: address PR review feedback - Add centralized backend config with path validation - Create apps/backend/core/config.py with get_worktree_base_path() - Validates paths to prevent .auto-claude/ and .git/ locations - Supports both relative and absolute paths (intentional for external drives) - Add frontend shared constants to reduce duplication - Create apps/frontend/src/shared/constants/worktree.ts - Export WORKTREE_BASE_PATH_VAR and DEFAULT_WORKTREE_PATH - Fix i18n compliance - replace all hardcoded strings with translation keys - Add worktree translations to en/settings.json and fr/settings.json - Update WorktreeSettings.tsx to use Trans component for formatted text - Update GeneralSettings.tsx heading to use translation key - Update all backend files to use centralized config - apps/backend/core/worktree.py - apps/backend/cli/batch_commands.py - apps/backend/cli/utils.py - apps/backend/core/workspace/git_utils.py - apps/backend/core/workspace/models.py - Update frontend to use shared constants - apps/frontend/src/main/project-store.ts Addresses CodeRabbit and Gemini Code Assist review comments on PR #456 --- apps/backend/cli/batch_commands.py | 4 +- apps/backend/cli/utils.py | 4 +- apps/backend/core/config.py | 61 +++++++++++++++++++ apps/backend/core/workspace/git_utils.py | 4 +- apps/backend/core/workspace/models.py | 4 +- apps/backend/core/worktree.py | 6 +- apps/frontend/src/main/project-store.ts | 3 +- .../project-settings/GeneralSettings.tsx | 2 +- .../project-settings/WorktreeSettings.tsx | 45 ++++++++++---- apps/frontend/src/shared/constants/index.ts | 3 + .../frontend/src/shared/constants/worktree.ts | 12 ++++ .../src/shared/i18n/locales/en/settings.json | 14 +++++ .../src/shared/i18n/locales/fr/settings.json | 14 +++++ 13 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 apps/backend/core/config.py create mode 100644 apps/frontend/src/shared/constants/worktree.ts diff --git a/apps/backend/cli/batch_commands.py b/apps/backend/cli/batch_commands.py index 3c0b0ed202..f1d027a279 100644 --- a/apps/backend/cli/batch_commands.py +++ b/apps/backend/cli/batch_commands.py @@ -184,8 +184,10 @@ def handle_batch_cleanup_command(project_dir: str, dry_run: bool = True) -> bool Returns: True if successful """ + from core.config import get_worktree_base_path + specs_dir = Path(project_dir) / ".auto-claude" / "specs" - worktree_base_path = os.getenv("WORKTREE_BASE_PATH", ".worktrees") + worktree_base_path = get_worktree_base_path(Path(project_dir)) worktrees_dir = Path(project_dir) / worktree_base_path if not specs_dir.exists(): diff --git a/apps/backend/cli/utils.py b/apps/backend/cli/utils.py index 84eddc0fa0..4e58e8ea0f 100644 --- a/apps/backend/cli/utils.py +++ b/apps/backend/cli/utils.py @@ -82,7 +82,9 @@ def find_spec(project_dir: Path, spec_identifier: str) -> Path | None: return spec_folder # Check worktree specs (for merge-preview, merge, review, discard operations) - worktree_base_path = os.getenv("WORKTREE_BASE_PATH", ".worktrees") + from core.config import get_worktree_base_path + + worktree_base_path = get_worktree_base_path(project_dir) worktree_base = project_dir / worktree_base_path if worktree_base.exists(): # Try exact match in worktree diff --git a/apps/backend/core/config.py b/apps/backend/core/config.py new file mode 100644 index 0000000000..077110523f --- /dev/null +++ b/apps/backend/core/config.py @@ -0,0 +1,61 @@ +""" +Core configuration for Auto Claude. + +Centralized configuration management including worktree paths and validation. +""" + +import os +from pathlib import Path + +# Environment variable names +WORKTREE_BASE_PATH_VAR = "WORKTREE_BASE_PATH" + +# Default values +DEFAULT_WORKTREE_PATH = ".worktrees" + + +def get_worktree_base_path(project_dir: Path | None = None) -> str: + """ + Gets the worktree base path from environment variables with validation. + + Supports: + - Relative paths (e.g., 'worktrees', '.cache/worktrees') + - Absolute paths (e.g., '/tmp/worktrees', 'C:\\worktrees') + + Prevents: + - Paths inside .auto-claude/ or .git/ + - Malicious relative paths escaping to system directories + + Args: + project_dir: Project root directory for validation. If None, only basic validation is performed. + + Returns: + The validated worktree base path string, or DEFAULT_WORKTREE_PATH if invalid. + """ + worktree_base_path = os.getenv(WORKTREE_BASE_PATH_VAR, DEFAULT_WORKTREE_PATH) + + # If no project_dir provided, return as-is (basic validation only) + if not project_dir: + # Check for obviously dangerous patterns + normalized = Path(worktree_base_path).as_posix() + if ".auto-claude" in normalized or ".git" in normalized: + return DEFAULT_WORKTREE_PATH + return worktree_base_path + + # Resolve the absolute path + if Path(worktree_base_path).is_absolute(): + resolved = Path(worktree_base_path).resolve() + else: + resolved = (project_dir / worktree_base_path).resolve() + + # Prevent paths inside .auto-claude/ or .git/ + auto_claude_dir = (project_dir / ".auto-claude").resolve() + git_dir = (project_dir / ".git").resolve() + + resolved_str = str(resolved) + if resolved_str.startswith(str(auto_claude_dir)) or resolved_str.startswith( + str(git_dir) + ): + return DEFAULT_WORKTREE_PATH + + return worktree_base_path diff --git a/apps/backend/core/workspace/git_utils.py b/apps/backend/core/workspace/git_utils.py index fc43c62c7f..e320671f54 100644 --- a/apps/backend/core/workspace/git_utils.py +++ b/apps/backend/core/workspace/git_utils.py @@ -222,8 +222,10 @@ def get_existing_build_worktree(project_dir: Path, spec_name: str) -> Path | Non Returns: Path to the worktree if it exists for this spec, None otherwise """ + from core.config import get_worktree_base_path + # Per-spec worktree path: .worktrees/{spec-name}/ (or custom WORKTREE_BASE_PATH) - worktree_base_path = os.getenv("WORKTREE_BASE_PATH", ".worktrees") + worktree_base_path = get_worktree_base_path(project_dir) worktree_path = project_dir / worktree_base_path / spec_name if worktree_path.exists(): return worktree_path diff --git a/apps/backend/core/workspace/models.py b/apps/backend/core/workspace/models.py index c64c01e748..f60d1647f2 100644 --- a/apps/backend/core/workspace/models.py +++ b/apps/backend/core/workspace/models.py @@ -250,7 +250,9 @@ def get_next_spec_number(self) -> int: max_number = max(max_number, self._scan_specs_dir(main_specs_dir)) # 2. Scan all worktree specs - worktree_base_path = os.getenv("WORKTREE_BASE_PATH", ".worktrees") + from core.config import get_worktree_base_path + + worktree_base_path = get_worktree_base_path(self.project_dir) worktrees_dir = self.project_dir / worktree_base_path if worktrees_dir.exists(): for worktree in worktrees_dir.iterdir(): diff --git a/apps/backend/core/worktree.py b/apps/backend/core/worktree.py index 890bde5ddf..12a18809ce 100644 --- a/apps/backend/core/worktree.py +++ b/apps/backend/core/worktree.py @@ -53,11 +53,13 @@ class WorktreeManager: """ def __init__(self, project_dir: Path, base_branch: str | None = None): + from core.config import get_worktree_base_path + self.project_dir = project_dir self.base_branch = base_branch or self._detect_base_branch() - # Use custom worktree path from environment variable, default to .worktrees - worktree_base_path = os.getenv("WORKTREE_BASE_PATH", ".worktrees") + # Use custom worktree path from environment variable with validation + worktree_base_path = get_worktree_base_path(project_dir) self.worktrees_dir = project_dir / worktree_base_path self._merge_lock = asyncio.Lock() diff --git a/apps/frontend/src/main/project-store.ts b/apps/frontend/src/main/project-store.ts index 48676b41c0..8685bc59d8 100644 --- a/apps/frontend/src/main/project-store.ts +++ b/apps/frontend/src/main/project-store.ts @@ -264,7 +264,8 @@ export class ProjectStore { // NOTE FOR MAINTAINERS: Worktree tasks are only included if the spec also exists in main. // This prevents deleted tasks from "coming back" when the worktree isn't cleaned up. // Alternative behavior: include all worktree tasks (remove the mainSpecIds check below). - const worktreeBasePath = process.env.WORKTREE_BASE_PATH || '.worktrees'; + import { WORKTREE_BASE_PATH_VAR, DEFAULT_WORKTREE_PATH } from '../shared/constants'; + const worktreeBasePath = process.env[WORKTREE_BASE_PATH_VAR] || DEFAULT_WORKTREE_PATH; const worktreesDir = path.join(project.path, worktreeBasePath); if (existsSync(worktreesDir)) { try { diff --git a/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx b/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx index 63e3253e52..0e7f33330f 100644 --- a/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx +++ b/apps/frontend/src/renderer/components/project-settings/GeneralSettings.tsx @@ -158,7 +158,7 @@ export function GeneralSettings({ {/* Worktree Location */}
-

Worktree Location

+

{t('projectSections.worktree.title')}

{ @@ -69,13 +70,13 @@ export function WorktreeSettings({ ? (isAbsolutePath(worktreePath) ? worktreePath : joinPath(projectPath, worktreePath)) - : joinPath(projectPath, '.worktrees'); + : joinPath(projectPath, DEFAULT_WORKTREE_PATH); const handleBrowse = async () => { const result = await window.electronAPI.dialog.showOpenDialog({ properties: ['openDirectory', 'createDirectory'], defaultPath: projectPath, - title: 'Select Worktree Location' + title: t('settings:worktree.selectLocation') }); if (!result.canceled && result.filePaths.length > 0) { @@ -95,17 +96,21 @@ export function WorktreeSettings({
-

Worktrees are isolated Git branches where Auto Claude builds features safely.

-

Default: .worktrees/ in your project root

+

{t('settings:worktree.description')}

+

+ + Default: .worktrees/ in your project root + +

- +
updateEnvConfig({ worktreePath: e.target.value })} /> @@ -119,21 +124,35 @@ export function WorktreeSettings({

- Supports relative paths (e.g., worktrees) or absolute paths (e.g., /tmp/worktrees) + + Supports relative paths (e.g., worktrees) or absolute paths (e.g., /tmp/worktrees) +

-

Resolved Path:

+

{t('settings:worktree.resolvedPath')}

{resolvedPath}
-

Common Use Cases:

+

{t('settings:worktree.commonUseCases')}

    -
  • External drive: /Volumes/FastSSD/worktrees
  • -
  • Temp directory: /tmp/my-project-worktrees
  • -
  • Shared builds: ../shared-worktrees
  • +
  • + + External drive: /Volumes/FastSSD/worktrees + +
  • +
  • + + Temp directory: /tmp/my-project-worktrees + +
  • +
  • + + Shared builds: ../shared-worktrees + +
diff --git a/apps/frontend/src/shared/constants/index.ts b/apps/frontend/src/shared/constants/index.ts index ea90dce632..89579d5179 100644 --- a/apps/frontend/src/shared/constants/index.ts +++ b/apps/frontend/src/shared/constants/index.ts @@ -32,3 +32,6 @@ export * from './github'; // Configuration and paths export * from './config'; + +// Worktree configuration constants +export * from './worktree'; diff --git a/apps/frontend/src/shared/constants/worktree.ts b/apps/frontend/src/shared/constants/worktree.ts new file mode 100644 index 0000000000..3841d6d438 --- /dev/null +++ b/apps/frontend/src/shared/constants/worktree.ts @@ -0,0 +1,12 @@ +/** + * Worktree configuration constants. + * + * Centralized constants for worktree path configuration to ensure consistency + * across frontend components and avoid duplication. + */ + +/** Environment variable name for custom worktree base path */ +export const WORKTREE_BASE_PATH_VAR = 'WORKTREE_BASE_PATH'; + +/** Default worktree base path (relative to project root) */ +export const DEFAULT_WORKTREE_PATH = '.worktrees'; diff --git a/apps/frontend/src/shared/i18n/locales/en/settings.json b/apps/frontend/src/shared/i18n/locales/en/settings.json index f9196432c5..0b86ffd266 100644 --- a/apps/frontend/src/shared/i18n/locales/en/settings.json +++ b/apps/frontend/src/shared/i18n/locales/en/settings.json @@ -214,6 +214,20 @@ "integrationTitle": "Memory", "integrationDescription": "Configure persistent cross-session memory for agents", "syncDescription": "Configure persistent memory" + }, + "worktree": { + "title": "Worktree Location", + "description": "Worktrees are isolated Git branches where Auto Claude builds features safely.", + "defaultInfo": "Default: <1>.worktrees/ in your project root", + "basePathLabel": "Worktree Base Path", + "basePathPlaceholder": ".worktrees (default)", + "selectLocation": "Select Worktree Location", + "pathTypeDescription": "Supports relative paths (e.g., <1>worktrees) or absolute paths (e.g., <2>/tmp/worktrees)", + "resolvedPath": "Resolved Path:", + "commonUseCases": "Common Use Cases:", + "externalDrive": "External drive: <1>/Volumes/FastSSD/worktrees", + "tempDirectory": "Temp directory: <1>/tmp/my-project-worktrees", + "sharedBuilds": "Shared builds: <1>../shared-worktrees" } }, "agentProfile": { diff --git a/apps/frontend/src/shared/i18n/locales/fr/settings.json b/apps/frontend/src/shared/i18n/locales/fr/settings.json index 5adab02d22..5875afb08d 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/settings.json +++ b/apps/frontend/src/shared/i18n/locales/fr/settings.json @@ -214,6 +214,20 @@ "integrationTitle": "Mémoire", "integrationDescription": "Configurer la mémoire persistante inter-sessions pour les agents", "syncDescription": "Configurer la mémoire persistante" + }, + "worktree": { + "title": "Emplacement des Worktrees", + "description": "Les worktrees sont des branches Git isolées où Auto Claude construit les fonctionnalités en toute sécurité.", + "defaultInfo": "Par défaut : <1>.worktrees/ à la racine de votre projet", + "basePathLabel": "Chemin de Base des Worktrees", + "basePathPlaceholder": ".worktrees (par défaut)", + "selectLocation": "Sélectionner l'Emplacement des Worktrees", + "pathTypeDescription": "Prend en charge les chemins relatifs (par exemple, <1>worktrees) ou absolus (par exemple, <2>/tmp/worktrees)", + "resolvedPath": "Chemin Résolu :", + "commonUseCases": "Cas d'Usage Courants :", + "externalDrive": "Disque externe : <1>/Volumes/FastSSD/worktrees", + "tempDirectory": "Répertoire temporaire : <1>/tmp/my-project-worktrees", + "sharedBuilds": "Builds partagés : <1>../shared-worktrees" } }, "agentProfile": { From dd245455bf8358c4c2b8d5354a15f9a3bd8ada03 Mon Sep 17 00:00:00 2001 From: Scott Beardsley Date: Wed, 31 Dec 2025 17:56:12 -0500 Subject: [PATCH 5/6] docs: enhance docstrings in config.py to improve coverage - Add comprehensive module-level docstring with examples - Add inline docstrings for WORKTREE_BASE_PATH_VAR constant - Add inline docstrings for DEFAULT_WORKTREE_PATH constant - Enhance get_worktree_base_path() docstring with: - Detailed description of functionality - Supported path types section - Security validations section - Comprehensive Args and Returns sections - Multiple usage examples - Design decision note Addresses docstring coverage check (72.73% -> improved) --- apps/backend/core/config.py | 77 ++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/apps/backend/core/config.py b/apps/backend/core/config.py index 077110523f..1b010dcafa 100644 --- a/apps/backend/core/config.py +++ b/apps/backend/core/config.py @@ -1,7 +1,22 @@ """ Core configuration for Auto Claude. -Centralized configuration management including worktree paths and validation. +This module provides centralized configuration management for Auto Claude, +including worktree path resolution and validation. It ensures consistent +configuration access across the entire backend codebase. + +Constants: + WORKTREE_BASE_PATH_VAR (str): Environment variable name for custom worktree base path. + DEFAULT_WORKTREE_PATH (str): Default worktree directory name relative to project root. + +Example: + >>> from core.config import get_worktree_base_path + >>> from pathlib import Path + >>> + >>> # Get worktree path with validation + >>> project_dir = Path("/path/to/project") + >>> worktree_path = get_worktree_base_path(project_dir) + >>> full_path = project_dir / worktree_path """ import os @@ -9,28 +24,70 @@ # Environment variable names WORKTREE_BASE_PATH_VAR = "WORKTREE_BASE_PATH" +"""str: Environment variable name for configuring custom worktree base path. + +Users can set this environment variable in their project's .env file to specify +a custom location for worktree directories, supporting both relative and absolute paths. +""" # Default values DEFAULT_WORKTREE_PATH = ".worktrees" +"""str: Default worktree directory name. + +This is the fallback value used when WORKTREE_BASE_PATH is not set or when +validation fails (e.g., path points to .auto-claude/ or .git/ directories). +""" def get_worktree_base_path(project_dir: Path | None = None) -> str: """ - Gets the worktree base path from environment variables with validation. + Get the worktree base path from environment variables with security validation. - Supports: - - Relative paths (e.g., 'worktrees', '.cache/worktrees') - - Absolute paths (e.g., '/tmp/worktrees', 'C:\\worktrees') + This function reads the WORKTREE_BASE_PATH environment variable and validates + it to prevent security issues. It supports both relative and absolute paths, + enabling users to place worktrees on external drives or temporary directories. - Prevents: - - Paths inside .auto-claude/ or .git/ - - Malicious relative paths escaping to system directories + Supported path types: + - Relative paths: 'worktrees', '.cache/worktrees', '../shared-worktrees' + - Absolute paths: '/tmp/worktrees', '/Volumes/FastSSD/worktrees', 'C:\\worktrees' + + Security validations: + - Prevents paths inside .auto-claude/ directory (avoids data loss during cleanup) + - Prevents paths inside .git/ directory (avoids repository corruption) + - Falls back to DEFAULT_WORKTREE_PATH if validation fails Args: - project_dir: Project root directory for validation. If None, only basic validation is performed. + project_dir (Path | None): Project root directory for full validation. + If None, only basic pattern validation is performed (checks for + .auto-claude and .git in path string). If provided, performs + full path resolution and validation. Returns: - The validated worktree base path string, or DEFAULT_WORKTREE_PATH if invalid. + str: The validated worktree base path string. Returns DEFAULT_WORKTREE_PATH + ('.worktrees') if the configured path is invalid or dangerous. + + Examples: + >>> # Basic usage without validation + >>> path = get_worktree_base_path() + >>> print(path) + '.worktrees' + + >>> # Full validation with project directory + >>> from pathlib import Path + >>> project = Path("/home/user/my-project") + >>> path = get_worktree_base_path(project) + >>> full_path = project / path + + >>> # With WORKTREE_BASE_PATH="/tmp/worktrees" in environment + >>> os.environ['WORKTREE_BASE_PATH'] = '/tmp/worktrees' + >>> path = get_worktree_base_path(project) + >>> print(path) + '/tmp/worktrees' + + Note: + The function intentionally allows absolute paths outside the project + directory to support use cases like external drives or shared build + directories. This is a design decision, not a security flaw. """ worktree_base_path = os.getenv(WORKTREE_BASE_PATH_VAR, DEFAULT_WORKTREE_PATH) From 432bd64371d933382459d2acd69015cb6d6da706 Mon Sep 17 00:00:00 2001 From: Scott Beardsley Date: Wed, 31 Dec 2025 18:08:17 -0500 Subject: [PATCH 6/6] refactor: improve path validation and move imports to module level - Fix path validation in config.py to check path segments instead of substrings - Prevents false positives like 'worktrees/.git-backup' - Uses Path.parts to check for .auto-claude/.git as components - Move all get_worktree_base_path imports to module level for consistency - batch_commands.py: Move import from function to top - utils.py: Move import from function to top - git_utils.py: Move import from function to top - models.py: Move import from function to top - worktree.py: Move import from __init__ to top Addresses PR review feedback from CodeRabbit and Gemini Code Assist --- apps/backend/cli/batch_commands.py | 3 +-- apps/backend/cli/utils.py | 3 +-- apps/backend/core/config.py | 6 +++--- apps/backend/core/workspace/git_utils.py | 4 ++-- apps/backend/core/workspace/models.py | 4 ++-- apps/backend/core/worktree.py | 4 ++-- 6 files changed, 11 insertions(+), 13 deletions(-) diff --git a/apps/backend/cli/batch_commands.py b/apps/backend/cli/batch_commands.py index f1d027a279..d1e68c42dc 100644 --- a/apps/backend/cli/batch_commands.py +++ b/apps/backend/cli/batch_commands.py @@ -9,6 +9,7 @@ import os from pathlib import Path +from core.config import get_worktree_base_path from ui import highlight, print_status @@ -184,8 +185,6 @@ def handle_batch_cleanup_command(project_dir: str, dry_run: bool = True) -> bool Returns: True if successful """ - from core.config import get_worktree_base_path - specs_dir = Path(project_dir) / ".auto-claude" / "specs" worktree_base_path = get_worktree_base_path(Path(project_dir)) worktrees_dir = Path(project_dir) / worktree_base_path diff --git a/apps/backend/cli/utils.py b/apps/backend/cli/utils.py index 4e58e8ea0f..820d3178a4 100644 --- a/apps/backend/cli/utils.py +++ b/apps/backend/cli/utils.py @@ -15,6 +15,7 @@ sys.path.insert(0, str(_PARENT_DIR)) from core.auth import get_auth_token, get_auth_token_source +from core.config import get_worktree_base_path from dotenv import load_dotenv from graphiti_config import get_graphiti_status from linear_integration import LinearManager @@ -82,8 +83,6 @@ def find_spec(project_dir: Path, spec_identifier: str) -> Path | None: return spec_folder # Check worktree specs (for merge-preview, merge, review, discard operations) - from core.config import get_worktree_base_path - worktree_base_path = get_worktree_base_path(project_dir) worktree_base = project_dir / worktree_base_path if worktree_base.exists(): diff --git a/apps/backend/core/config.py b/apps/backend/core/config.py index 1b010dcafa..fb24c832d8 100644 --- a/apps/backend/core/config.py +++ b/apps/backend/core/config.py @@ -93,9 +93,9 @@ def get_worktree_base_path(project_dir: Path | None = None) -> str: # If no project_dir provided, return as-is (basic validation only) if not project_dir: - # Check for obviously dangerous patterns - normalized = Path(worktree_base_path).as_posix() - if ".auto-claude" in normalized or ".git" in normalized: + # Check for .auto-claude or .git as path components, not substrings + parts = set(Path(worktree_base_path).parts) + if ".auto-claude" in parts or ".git" in parts: return DEFAULT_WORKTREE_PATH return worktree_base_path diff --git a/apps/backend/core/workspace/git_utils.py b/apps/backend/core/workspace/git_utils.py index e320671f54..3407d2e358 100644 --- a/apps/backend/core/workspace/git_utils.py +++ b/apps/backend/core/workspace/git_utils.py @@ -11,6 +11,8 @@ import subprocess from pathlib import Path +from core.config import get_worktree_base_path + # Constants for merge limits MAX_FILE_LINES_FOR_AI = 5000 # Skip AI for files larger than this MAX_PARALLEL_AI_MERGES = 5 # Limit concurrent AI merge operations @@ -222,8 +224,6 @@ def get_existing_build_worktree(project_dir: Path, spec_name: str) -> Path | Non Returns: Path to the worktree if it exists for this spec, None otherwise """ - from core.config import get_worktree_base_path - # Per-spec worktree path: .worktrees/{spec-name}/ (or custom WORKTREE_BASE_PATH) worktree_base_path = get_worktree_base_path(project_dir) worktree_path = project_dir / worktree_base_path / spec_name diff --git a/apps/backend/core/workspace/models.py b/apps/backend/core/workspace/models.py index f60d1647f2..f538bb2458 100644 --- a/apps/backend/core/workspace/models.py +++ b/apps/backend/core/workspace/models.py @@ -11,6 +11,8 @@ from enum import Enum from pathlib import Path +from core.config import get_worktree_base_path + class WorkspaceMode(Enum): """How auto-claude should work.""" @@ -250,8 +252,6 @@ def get_next_spec_number(self) -> int: max_number = max(max_number, self._scan_specs_dir(main_specs_dir)) # 2. Scan all worktree specs - from core.config import get_worktree_base_path - worktree_base_path = get_worktree_base_path(self.project_dir) worktrees_dir = self.project_dir / worktree_base_path if worktrees_dir.exists(): diff --git a/apps/backend/core/worktree.py b/apps/backend/core/worktree.py index 12a18809ce..7b11b7b162 100644 --- a/apps/backend/core/worktree.py +++ b/apps/backend/core/worktree.py @@ -22,6 +22,8 @@ from dataclasses import dataclass from pathlib import Path +from core.config import get_worktree_base_path + class WorktreeError(Exception): """Error during worktree operations.""" @@ -53,8 +55,6 @@ class WorktreeManager: """ def __init__(self, project_dir: Path, base_branch: str | None = None): - from core.config import get_worktree_base_path - self.project_dir = project_dir self.base_branch = base_branch or self._detect_base_branch()