diff --git a/.gitignore b/.gitignore index f7e4711e..28b23f32 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,4 @@ _bmad-output/ /docs OPUS_ANALYSIS_AND_IDEAS.md /.github/agents +/config.json diff --git a/apps/backend/core/client.py b/apps/backend/core/client.py index c50978ee..761cf2cf 100644 --- a/apps/backend/core/client.py +++ b/apps/backend/core/client.py @@ -545,6 +545,48 @@ def create_client( # cases where Claude uses absolute paths for file operations project_path_str = str(project_dir.resolve()) spec_path_str = str(spec_dir.resolve()) + + # Detect if we're running in a worktree and get the original project directory + # Worktrees are located in either: + # - .auto-claude/worktrees/tasks/{spec-name}/ (new location) + # - .worktrees/{spec-name}/ (legacy location) + # When running in a worktree, we need to allow access to both the worktree + # and the original project's .auto-claude/ directory for spec files + original_project_permissions = [] + resolved_project_path = project_dir.resolve() + + # Check for worktree paths and extract original project directory + # This handles spec worktrees, PR review worktrees, and legacy worktrees + # Note: Windows paths are normalized to forward slashes before comparison + worktree_markers = [ + "/.auto-claude/worktrees/tasks/", # Spec/task worktrees + "/.auto-claude/github/pr/worktrees/", # PR review worktrees + "/.worktrees/", # Legacy worktree location + ] + project_path_posix = str(resolved_project_path).replace("\\", "/") + + for marker in worktree_markers: + if marker in project_path_posix: + # Extract the original project directory (parent of worktree location) + # Use rsplit to get the rightmost occurrence (handles nested projects) + original_project_str = project_path_posix.rsplit(marker, 1)[0] + original_project_dir = Path(original_project_str) + + # Grant permissions for relevant directories in the original project + permission_ops = ["Read", "Write", "Edit", "Glob", "Grep"] + dirs_to_permit = [ + original_project_dir / ".auto-claude", + original_project_dir / ".worktrees", # Legacy support + ] + + for dir_path in dirs_to_permit: + if dir_path.exists(): + path_str = str(dir_path.resolve()) + original_project_permissions.extend( + [f"{op}({path_str}/**)" for op in permission_ops] + ) + break + security_settings = { "sandbox": {"enabled": True, "autoAllowBashIfSandboxed": True}, "permissions": { @@ -567,6 +609,9 @@ def create_client( f"Read({spec_path_str}/**)", f"Write({spec_path_str}/**)", f"Edit({spec_path_str}/**)", + # Allow original project's .auto-claude/ and .worktrees/ directories + # when running in a worktree (fixes issue #385 - permission errors) + *original_project_permissions, # Bash permission granted here, but actual commands are validated # by the bash_security_hook (see security.py for allowed commands) "Bash(*)", @@ -603,6 +648,8 @@ def create_client( print(f"Security settings: {settings_file}") print(" - Sandbox enabled (OS-level bash isolation)") print(f" - Filesystem restricted to: {project_dir.resolve()}") + if original_project_permissions: + print(" - Worktree permissions: granted for original project directories") print(" - Bash commands restricted to allowlist") if max_thinking_tokens: print(f" - Extended thinking: {max_thinking_tokens:,} tokens")