shareAI-lab/learn-claude-code · error · ValueError

Worktree path escapes directory: {name!r}

Error message

Worktree path escapes directory: {name!r}

What it means

_worktree_path() maps a worktree name to WORKTREES_DIR/name and requires the resolved path to stay strictly inside WORKTREES_ROOT (and not equal the root itself). It complements validate_worktree_name(): even a name that passes character validation is rejected here if resolution (symlinks, WORKTREES_DIR being a symlink, or WORKDIR itself moving) pushes the path outside the workspace.

Source

Thrown at s13_agent_teams/code.py:318

WORKTREES_ROOT = WORKTREES_DIR.resolve()
VALID_WORKTREE_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")


def validate_worktree_name(name: str) -> str | None:
    if not isinstance(name, str) or not VALID_WORKTREE_NAME.fullmatch(name):
        return ("worktree name must be 1-64 letters, digits, dots, "
                "underscores, or dashes, and start with a letter or digit")
    if name in {".", ".."} or ".." in name:
        return "worktree name cannot contain '..'"
    return None


def _worktree_path(name: str) -> Path:
    path = (WORKTREES_DIR / name).resolve()
    if (not WORKTREES_ROOT.is_relative_to(WORKDIR.resolve())
            or not path.is_relative_to(WORKTREES_ROOT)
            or path == WORKTREES_ROOT):
        raise ValueError(f"Worktree path escapes directory: {name!r}")
    return path


def _worktree_branch(name: str) -> str:
    return f"wt/{name}"


def _run_git(args: list[str], cwd: Path | None = None) -> tuple[bool, str]:
    """Run Git without shell interpolation and preserve machine output."""
    try:
        result = subprocess.run(
            ["git", *args], cwd=cwd or WORKDIR,
            capture_output=True, text=True, timeout=30,
        )
    except (OSError, subprocess.TimeoutExpired) as exc:
        return False, f"{type(exc).__name__}: {exc}"
    output = (result.stdout + result.stderr).strip()
    return result.returncode == 0, output or "(no output)"

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Make .worktrees a real directory inside WORKDIR; remove any symlink indirection.
  2. Delete foreign symlinked entries under .worktrees and let the tool create real git worktrees.
  3. Resolve WORKDIR at startup so root-containment checks are stable.

Example fix

# before
ln -s /mnt/pool/wt .worktrees  # every _worktree_path raises

# after
rm .worktrees && mkdir .worktrees
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def worktree_path_safe(name: str) -> bool:
    path = (WORKTREES_DIR / name).resolve()
    return path.is_relative_to(WORKTREES_ROOT) and path != WORKTREES_ROOT

Try / catch

try:
    wt = _worktree_path(name)
except ValueError:
    logging.error('worktree %r unsafe (symlink under .worktrees?); recreating', name)
    raise

Prevention

When it happens

Trigger: A worktree entry under .worktrees/ is a symlink pointing elsewhere; .worktrees itself is a symlink to outside the workspace; WORKDIR is re-symlinked after import; name resolving exactly to the .worktrees root.

Common situations: Users pre-creating .worktrees entries as symlinks into another checkout; macOS /tmp symlink resolution mismatch; shared worktree pools on another disk.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/e4a6ed5af242c347. Report an issue: GitHub.