{"record":{"id":"e4a6ed5af242c347","repo":"shareAI-lab/learn-claude-code","slug":"worktree-path-escapes-directory-name-r","errorCode":null,"errorMessage":"Worktree path escapes directory: {name!r}","messagePattern":"Worktree path escapes directory: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"s13_agent_teams/code.py","lineNumber":318,"sourceCode":"WORKTREES_ROOT = WORKTREES_DIR.resolve()\nVALID_WORKTREE_NAME = re.compile(r\"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$\")\n\n\ndef validate_worktree_name(name: str) -> str | None:\n    if not isinstance(name, str) or not VALID_WORKTREE_NAME.fullmatch(name):\n        return (\"worktree name must be 1-64 letters, digits, dots, \"\n                \"underscores, or dashes, and start with a letter or digit\")\n    if name in {\".\", \"..\"} or \"..\" in name:\n        return \"worktree name cannot contain '..'\"\n    return None\n\n\ndef _worktree_path(name: str) -> Path:\n    path = (WORKTREES_DIR / name).resolve()\n    if (not WORKTREES_ROOT.is_relative_to(WORKDIR.resolve())\n            or not path.is_relative_to(WORKTREES_ROOT)\n            or path == WORKTREES_ROOT):\n        raise ValueError(f\"Worktree path escapes directory: {name!r}\")\n    return path\n\n\ndef _worktree_branch(name: str) -> str:\n    return f\"wt/{name}\"\n\n\ndef _run_git(args: list[str], cwd: Path | None = None) -> tuple[bool, str]:\n    \"\"\"Run Git without shell interpolation and preserve machine output.\"\"\"\n    try:\n        result = subprocess.run(\n            [\"git\", *args], cwd=cwd or WORKDIR,\n            capture_output=True, text=True, timeout=30,\n        )\n    except (OSError, subprocess.TimeoutExpired) as exc:\n        return False, f\"{type(exc).__name__}: {exc}\"\n    output = (result.stdout + result.stderr).strip()\n    return result.returncode == 0, output or \"(no output)\"","sourceCodeStart":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s13_agent_teams/code.py#L300-L336","documentation":"_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.","triggerScenarios":"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.","commonSituations":"Users pre-creating .worktrees entries as symlinks into another checkout; macOS /tmp symlink resolution mismatch; shared worktree pools on another disk.","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."],"exampleFix":"# before\nln -s /mnt/pool/wt .worktrees  # every _worktree_path raises\n\n# after\nrm .worktrees && mkdir .worktrees","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef worktree_path_safe(name: str) -> bool:\n    path = (WORKTREES_DIR / name).resolve()\n    return path.is_relative_to(WORKTREES_ROOT) and path != WORKTREES_ROOT","typeGuard":null,"tryCatchPattern":"try:\n    wt = _worktree_path(name)\nexcept ValueError:\n    logging.error('worktree %r unsafe (symlink under .worktrees?); recreating', name)\n    raise","preventionTips":["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."],"tags":["path-traversal","git-worktree","symlink","security"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}