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
- Make .worktrees a real directory inside WORKDIR; remove any symlink indirection.
- Delete foreign symlinked entries under .worktrees and let the tool create real git worktrees.
- 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
- Keep .worktrees a real directory; never symlink it or its entries outside the workspace.
- Validate worktree names with validate_worktree_name() before use.
- Resolve WORKDIR at startup.
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
- Memory path escapes the store: {filename}
- Tasks directory escapes workspace
- Mailbox path escapes directory: {agent!r}
- Path escapes workspace: {p}
- Path escapes workspace: {p}
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/e4a6ed5af242c347.
Report an issue: GitHub.