abhigyanpatwari/GitNexus · critical · ValueError

candidate destination escapes the clone: {relative}

Error message

candidate destination escapes the clone: {relative}

What it means

Thrown by _replace_regular_file (evolution.py:170) before writing an overlay file into the clone. It rejects any relative path that is_absolute(), has no parts, or contains a '..' component. This is the path-traversal guard preventing a candidate overlay from writing outside the clone root through a relative destination.

Source

Thrown at eval/workflow_bench/evolution.py:170

    return _fingerprint_payload(payload), payload


def _fingerprint_payload(payload: list[tuple[PurePosixPath, bytes]]) -> str:
    digest = hashlib.sha256()
    for relative_path, content in payload:
        relative = relative_path.as_posix().encode()
        digest.update(len(relative).to_bytes(8, "big"))
        digest.update(relative)
        digest.update(len(content).to_bytes(8, "big"))
        digest.update(content)
    return digest.hexdigest()


def _replace_regular_file(root: Path, relative: Path, content: bytes) -> None:
    """Replace a clone file through validated directory descriptors."""

    if relative.is_absolute() or not relative.parts or ".." in relative.parts:
        raise ValueError(f"candidate destination escapes the clone: {relative}")
    _require_real_directory(root, label="candidate destination root")
    directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
    descriptor = os.open(root, directory_flags)
    try:
        for part in relative.parts[:-1]:
            try:
                os.mkdir(part, mode=0o700, dir_fd=descriptor)
            except FileExistsError:
                pass
            try:
                child = os.open(part, directory_flags, dir_fd=descriptor)
            except OSError as exc:
                raise ValueError(
                    f"candidate destination parent must be a real directory: {relative.parent}: {exc}"
                ) from exc
            os.close(descriptor)
            descriptor = child

View on GitHub (pinned to d540b00184)

Solutions

  1. Remove all '..' and absolute components from overlay paths; keep paths as clean relative POSIX paths under .claude/skills/.
  2. Sanitize with PurePosixPath: reject p.is_absolute() or '..' in p.parts, then pass only the normalized result.
  3. Re-create the overlay from a clean checkout so no stray traversal components remain.

Example fix

# before: overlay contains a traversal path
.claude/skills/../../package.json

# after: keep only real skill paths; guard before applying
from pathlib import PurePosixPath
rel = PurePosixPath('.claude/skills/gitnexus-work/SKILL.md')
assert not rel.is_absolute() and rel.parts and '..' not in rel.parts, 'escapes clone'
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import PurePosixPath

def overlay_path_is_safe(relative: PurePosixPath) -> bool:
    return (
        not relative.is_absolute()
        and bool(relative.parts)
        and '..' not in relative.parts
        and '' not in relative.parts
    )

Type guard

from pathlib import PurePosixPath

def is_safe_overlay_path(p: PurePosixPath) -> bool:
    return not p.is_absolute() and bool(p.parts) and '..' not in p.parts and '' not in p.parts

Try / catch

try:
    apply_candidate_overlay(overlay, worktree, sandbox=sandbox)
except ValueError as exc:
    if 'escapes the clone' in str(exc):
        raise SystemExit(f'refuse: overlay path traversal detected: {exc}')

Prevention

When it happens

Trigger: An overlay file whose repo-relative path includes '..' (e.g. .claude/skills/../../etc/passwd), an absolute path (/etc/x), or an empty leaf name. Any such destination would escape the clone and is refused before any directory-fd walk begins.

Common situations: Manually crafting an overlay that reuses an existing repo path with a '../' shortcut; a buggy script that builds overlay paths from user input without normalization; an adversarial candidate attempting to escape the trust boundary.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12). Data as JSON: /api/errors/0c9e46a3b9021127. Report an issue: GitHub.