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 refuses the result unless the resolved path stays strictly inside WORKTREES_ROOT and is not the root itself. This blocks traversal via names that survive _validate_worktree_name (e.g. names containing separators after odd normalization) and symlinked worktrees pointing elsewhere.

Source

Thrown at s15_integrated_harness/code.py:391

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 return (ok, combined 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. Pass only a bare name matching the validator (1-64 chars of letters/digits/dots/underscores/dashes, no '..', leading alnum).
  2. Remove symlinks inside the worktrees directory; worktrees must be real git worktrees created by the harness.
  3. Keep WORKTREES_DIR physically under WORKDIR.

Example fix

// before
worktree_cwd_for("../shared-build")

// after
worktree_cwd_for("shared-build")  // bare name; harness resolves it under WORKTREES_DIR
Defensive patterns

Strategy: validation

Validate before calling

import re

WORKTREE_NAME_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}")

def is_valid_worktree_name(name: str) -> bool:
    return (
        isinstance(name, str)
        and bool(WORKTREE_NAME_RE.fullmatch(name))
        and name not in {".", ".."}
        and ".." not in name
    )

Type guard

def is_bare_worktree_name(name) -> bool:
    return isinstance(name, str) and "/" not in name and "\\" not in name and name not in {".", ".."} and ".." not in name

Try / catch

try:
    path = _worktree_path(name)
except ValueError as e:
    if "escapes directory" in str(e):
        return f"Error: invalid worktree name {name!r}"
    raise

Prevention

When it happens

Trigger: Passing a worktree name that, once joined and resolved, lands outside the worktrees root — e.g. an absolute path (the join semantics can flip), a name whose target inside WORKTREES_DIR is a symlink to another location, or a name resolving exactly to WORKTREES_ROOT itself.

Common situations: Model tool calls passing a path instead of a bare name; pre-created symlinks inside the worktrees directory; moving WORKTREES_DIR outside WORKDIR in config.

Related errors


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