fix: add worktree isolation warnings to prevent agent escape (ACS-394) (#1495)

* fix: add worktree isolation warnings to prevent agent escape (ACS-394)

Agents were escaping isolated worktrees by using `cd` to navigate to
parent project paths, causing git commits to go to the wrong branch and
files to be created/modified in the wrong location.

This fix adds:
- detect_worktree_mode() function to detect worktree isolation
- Worktree isolation warning section injected into agent prompts
- Explicit forbidden parent path display to prevent `cd` commands
- Comprehensive test coverage (11 tests) for Unix/Windows paths

Modified files:
- apps/backend/prompts_pkg/prompt_generator.py
- apps/backend/prompts/coder.md
- apps/backend/prompts/qa_fixer.md

New files:
- tests/test_prompt_generator.py

Refs: ACS-394

* refactor: simplify worktree detection and address PR review comments

- Normalize path separators to forward slashes for consistent matching
- Use split with maxsplit=1 instead of chained split calls
- Extract path patterns to named constants for clarity
- Remove unused pytest import from test file

Addresses review comments from:
- GitHub Advanced Security: unused import
- Gemini code-assist: simplify path splitting logic

All 11 tests continue to pass.

* refactor: address Auto Claude PR review findings

- Remove redundant sys.path.insert (conftest.py already handles this)
- Remove hardcoded directory examples from worktree warning
- Update test to verify warning content without specific paths
- Simplify worktree isolation warning to be project-agnostic

Addresses Auto Claude PR Review findings:
- [LOW] Redundant sys.path.insert - removed
- [LOW] Hardcoded directory examples - made generic

All 11 tests pass.

* refactor: remove unused project_dir parameter from detect_worktree_mode

The project_dir parameter was accepted but never used in the function body.
Worktree detection only examines spec_dir to detect worktree path patterns.

Updated:
- Function signature to remove unused parameter
- Docstring to remove parameter documentation
- Call site in generate_environment_context()
- All 6 test calls

Addresses CodeRabbitAI review comment.

All 11 tests pass.

---------

Co-authored-by: StillKnotKnown <stillknotknown@users.noreply.github.com>
Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
StillKnotKnown
2026-01-26 13:48:28 +02:00
committed by GitHub
parent f6b264d562
commit 1e453653b2
4 changed files with 335 additions and 1 deletions
+59
View File
@@ -84,6 +84,65 @@ git add [verified-path]
---
## 🚨 CRITICAL: WORKTREE ISOLATION 🚨
**You may be in an ISOLATED GIT WORKTREE environment.**
Check the "YOUR ENVIRONMENT" section at the top of this prompt. If you see an
**"ISOLATED WORKTREE - CRITICAL"** section, you are in a worktree.
### What is a Worktree?
A worktree is a **complete copy of the project** isolated from the main project.
This allows safe development without affecting the main branch.
### Worktree Rules (CRITICAL)
**If you are in a worktree, the environment section will show:**
* **YOUR LOCATION:** The path to your isolated worktree
* **FORBIDDEN:** The parent project path you must NEVER `cd` to
**CRITICAL RULES:**
* **NEVER** `cd` to the forbidden parent path
* **NEVER** use `cd ../..` to escape the worktree
* **STAY** within your working directory at all times
* **ALL** file operations use paths relative to your current location
### Why This Matters
Escaping the worktree causes:
* ❌ Git commits going to the wrong branch
* ❌ Files created/modified in the wrong location
* ❌ Breaking worktree isolation guarantees
* ❌ Losing the safety of isolated development
### How to Stay Safe
**Before ANY `cd` command:**
```bash
# 1. Check where you are
pwd
# 2. Verify the target is within your worktree
# If pwd shows: /path/to/.auto-claude/worktrees/tasks/spec-name/
# Then: cd ./apps/backend ✅ SAFE
# But: cd /path/to/parent/project ❌ FORBIDDEN - ESCAPES ISOLATION
# 3. When in doubt, don't use cd at all
# Use relative paths from your current directory instead
git add ./apps/backend/file.py # Works from anywhere in worktree
```
### The Golden Rule in Worktrees
**If you're in a worktree, pretend the parent project doesn't exist.**
Everything you need is in your worktree, accessible via relative paths.
---
## STEP 1: GET YOUR BEARINGS (MANDATORY)
First, check your environment. The prompt should tell you your working directory and spec location.
+59
View File
@@ -142,6 +142,65 @@ git add [verified-path]
---
## 🚨 CRITICAL: WORKTREE ISOLATION 🚨
**You may be in an ISOLATED GIT WORKTREE environment.**
Check the "YOUR ENVIRONMENT" section at the top of this prompt. If you see an
**"ISOLATED WORKTREE - CRITICAL"** section, you are in a worktree.
### What is a Worktree?
A worktree is a **complete copy of the project** isolated from the main project.
This allows safe development without affecting the main branch.
### Worktree Rules (CRITICAL)
**If you are in a worktree, the environment section will show:**
* **YOUR LOCATION:** The path to your isolated worktree
* **FORBIDDEN:** The parent project path you must NEVER `cd` to
**CRITICAL RULES:**
* **NEVER** `cd` to the forbidden parent path
* **NEVER** use `cd ../..` to escape the worktree
* **STAY** within your working directory at all times
* **ALL** file operations use paths relative to your current location
### Why This Matters
Escaping the worktree causes:
* ❌ Git commits going to the wrong branch
* ❌ Files created/modified in the wrong location
* ❌ Breaking worktree isolation guarantees
* ❌ Losing the safety of isolated development
### How to Stay Safe
**Before ANY `cd` command:**
```bash
# 1. Check where you are
pwd
# 2. Verify the target is within your worktree
# If pwd shows: /path/to/.auto-claude/worktrees/tasks/spec-name/
# Then: cd ./apps/backend ✅ SAFE
# But: cd /path/to/parent/project ❌ FORBIDDEN - ESCAPES ISOLATION
# 3. When in doubt, don't use cd at all
# Use relative paths from your current directory instead
git add ./apps/backend/file.py # Works from anywhere in worktree
```
### The Golden Rule in Worktrees
**If you're in a worktree, pretend the parent project doesn't exist.**
Everything you need is in your worktree, accessible via relative paths.
---
## PHASE 3: FIX ISSUES ONE BY ONE
For each issue in the fix request:
+64 -1
View File
@@ -16,6 +16,37 @@ import json
from pathlib import Path
def detect_worktree_mode(spec_dir: Path) -> tuple[bool, str | None]:
"""
Detect if running in isolated worktree mode.
Args:
spec_dir: Absolute path to spec directory
Returns:
(is_worktree, forbidden_parent_path) tuple:
- is_worktree: True if running in a worktree
- forbidden_parent_path: The parent project path to forbid, or None
"""
# Check if spec_dir contains worktree path patterns
# Normalize path separators to forward slashes for consistent matching
spec_str = str(spec_dir).replace("\\", "/")
# New worktree location: .auto-claude/worktrees/tasks/{spec-name}/
new_worktree_marker = "/.auto-claude/worktrees/tasks/"
if new_worktree_marker in spec_str:
parent_path = spec_str.split(new_worktree_marker, 1)[0]
return True, parent_path
# Legacy worktree location: .worktrees/{spec-name}/
legacy_worktree_marker = "/.worktrees/"
if legacy_worktree_marker in spec_str:
parent_path = spec_str.split(legacy_worktree_marker, 1)[0]
return True, parent_path
return False, None
def get_relative_spec_path(spec_dir: Path, project_dir: Path) -> str:
"""
Get the spec directory path relative to the project/working directory.
@@ -54,7 +85,11 @@ def generate_environment_context(project_dir: Path, spec_dir: Path) -> str:
"""
relative_spec = get_relative_spec_path(spec_dir, project_dir)
return f"""## YOUR ENVIRONMENT
# Detect worktree mode and get forbidden parent path
is_worktree, forbidden_parent = detect_worktree_mode(spec_dir)
# Build the environment context
context = f"""## YOUR ENVIRONMENT
**Working Directory:** `{project_dir}`
**Spec Location:** `{relative_spec}/`
@@ -77,6 +112,34 @@ coder prompt for detailed examples.
"""
# Add worktree isolation warning if in worktree mode
if is_worktree and forbidden_parent:
context += f"""## 🚨 ISOLATED WORKTREE - CRITICAL
You are in an **ISOLATED GIT WORKTREE** - a complete copy of the project.
**YOUR LOCATION:** `{project_dir}`
**FORBIDDEN:** Do NOT use `cd {forbidden_parent}` or `cd ../..` - this **ESCAPES ISOLATION**
All project files exist HERE via relative paths from your working directory.
**CRITICAL RULES:**
* **NEVER** `cd {forbidden_parent}` or any path traversal outside your worktree
* **STAY** within your working directory at all times
* **ALL** file operations use paths relative to your current location
* Before any `cd` command, run `pwd` and verify the target is within your worktree
**VIOLATION WARNING:** Escaping the worktree will cause:
* Git commits going to wrong branch
* Files created/modified in the wrong location
* Breaking worktree isolation guarantees
---
"""
return context
def generate_subtask_prompt(
spec_dir: Path,
+153
View File
@@ -0,0 +1,153 @@
"""
Tests for prompt_generator module functions.
Tests for worktree detection and environment context generation.
"""
from pathlib import Path
# Note: sys.path manipulation is handled by conftest.py line 46
from prompts_pkg.prompt_generator import detect_worktree_mode, generate_environment_context
class TestDetectWorktreeMode:
"""Tests for detect_worktree_mode function."""
def test_new_worktree_unix_path(self):
"""Test detection of new worktree location on Unix."""
# New worktree: /project/.auto-claude/worktrees/tasks/spec-name/.auto-claude/specs/spec/
spec_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/001-feature/.auto-claude/specs/001-feature")
project_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/001-feature")
is_worktree, forbidden = detect_worktree_mode(spec_dir)
assert is_worktree is True
assert forbidden == "/opt/dev/project"
def test_new_worktree_windows_path(self):
"""Test detection of new worktree location on Windows."""
# Windows path with backslashes
spec_dir = Path("E:/projects/x/.auto-claude/worktrees/tasks/009-audit/.auto-claude/specs/009-audit")
project_dir = Path("E:/projects/x/.auto-claude/worktrees/tasks/009-audit")
is_worktree, forbidden = detect_worktree_mode(spec_dir)
assert is_worktree is True
assert forbidden == "E:/projects/x"
def test_legacy_worktree_unix_path(self):
"""Test detection of legacy worktree location on Unix."""
# Legacy worktree: /project/.worktrees/spec-name/.auto-claude/specs/spec/
spec_dir = Path("/opt/dev/project/.worktrees/001-feature/.auto-claude/specs/001-feature")
project_dir = Path("/opt/dev/project/.worktrees/001-feature")
is_worktree, forbidden = detect_worktree_mode(spec_dir)
assert is_worktree is True
assert forbidden == "/opt/dev/project"
def test_legacy_worktree_windows_path(self):
"""Test detection of legacy worktree location on Windows."""
spec_dir = Path("C:/projects/x/.worktrees/009-audit/.auto-claude/specs/009-audit")
project_dir = Path("C:/projects/x/.worktrees/009-audit")
is_worktree, forbidden = detect_worktree_mode(spec_dir)
assert is_worktree is True
assert forbidden == "C:/projects/x"
def test_not_in_worktree(self):
"""Test when not in a worktree (direct mode)."""
# Direct mode: /project/.auto-claude/specs/spec/
spec_dir = Path("/opt/dev/project/.auto-claude/specs/001-feature")
project_dir = Path("/opt/dev/project")
is_worktree, forbidden = detect_worktree_mode(spec_dir)
assert is_worktree is False
assert forbidden is None
def test_deeply_nested_worktree(self):
"""Test worktree detection with deeply nested spec directory."""
spec_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/009-very-long-spec-name-for-testing/.auto-claude/specs/009-very-long-spec-name-for-testing")
project_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/009-very-long-spec-name-for-testing")
is_worktree, forbidden = detect_worktree_mode(spec_dir)
assert is_worktree is True
assert forbidden == "/opt/dev/project"
class TestGenerateEnvironmentContext:
"""Tests for generate_environment_context function."""
def test_context_includes_worktree_warning(self):
"""Test that worktree isolation warning is included when in worktree."""
spec_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/001-feature/.auto-claude/specs/001-feature")
project_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/001-feature")
context = generate_environment_context(project_dir, spec_dir)
# Verify worktree warning is present
assert "ISOLATED WORKTREE - CRITICAL" in context
assert "FORBIDDEN:" in context
assert "/opt/dev/project" in context
assert "ESCAPES ISOLATION" in context
def test_context_no_worktree_warning_in_direct_mode(self):
"""Test that worktree warning is NOT included in direct mode."""
spec_dir = Path("/opt/dev/project/.auto-claude/specs/001-feature")
project_dir = Path("/opt/dev/project")
context = generate_environment_context(project_dir, spec_dir)
# Verify worktree warning is NOT present
assert "ISOLATED WORKTREE - CRITICAL" not in context
assert "FORBIDDEN:" not in context
def test_context_includes_basic_environment(self):
"""Test that basic environment information is always included."""
spec_dir = Path("/opt/dev/project/.auto-claude/specs/001-feature")
project_dir = Path("/opt/dev/project")
context = generate_environment_context(project_dir, spec_dir)
# Verify basic sections
assert "## YOUR ENVIRONMENT" in context
assert "**Working Directory:**" in context
assert "**Spec Location:**" in context
assert "implementation_plan.json" in context
assert "PATH CONFUSION PREVENTION" in context
def test_context_windows_worktree(self):
"""Test worktree warning with Windows paths (from ticket ACS-394)."""
# This is the exact scenario from the bug report
spec_dir = Path("E:/projects/x/.auto-claude/worktrees/tasks/009-audit/.auto-claude/specs/009-audit")
project_dir = Path("E:/projects/x/.auto-claude/worktrees/tasks/009-audit")
context = generate_environment_context(project_dir, spec_dir)
# Verify worktree warning includes the Windows path
assert "ISOLATED WORKTREE - CRITICAL" in context
assert "E:/projects/x" in context
assert "cd E:/projects/x" in context or '"cd E:/projects/x"' in context
def test_context_forbidden_path_examples(self):
"""Test that forbidden path is shown and critical rules are included."""
spec_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/001-feature/.auto-claude/specs/001-feature")
project_dir = Path("/opt/dev/project/.auto-claude/worktrees/tasks/001-feature")
context = generate_environment_context(project_dir, spec_dir)
# Verify forbidden parent path is shown
assert "FORBIDDEN:" in context
assert "/opt/dev/project" in context # The parent project that is forbidden
# Verify CRITICAL RULES section exists
assert "**CRITICAL RULES:**" in context
assert "**NEVER**" in context # Explicit prohibition
# Verify violation warning explains consequences
assert "**VIOLATION WARNING:**" in context
assert "Git commits going to wrong branch" in context