abhigyanpatwari/GitNexus · error · SandboxError

{label} escapes its allowed repository root: {relative}

Error message

{label} escapes its allowed repository root: {relative}

What it means

Raised by _safe_repo_source when the relative path is structurally valid (not absolute, no '..') but lexical.resolve() lands outside the repository root — resolve().relative_to(repo) raised ValueError. This catches escapes that survive the structural check, e.g. a real symlink inside the repo that points outside, or a path component that resolves to a parent via a symlink.

Source

Thrown at eval/workflow_bench/proposer_sandbox.py:570

    try:
        resolved = lexical.resolve(strict=True)
    except OSError as exc:
        raise SandboxError(f"{label} must be a real directory: {lexical}: {exc}") from exc
    if resolved != lexical:
        raise SandboxError(f"{label} must not traverse symlinks: {lexical}")
    return lexical


def _safe_repo_source(repo: Path, relative: str, *, label: str) -> tuple[Path, Path]:
    candidate = PurePosixPath(relative)
    if candidate.is_absolute() or ".." in candidate.parts or not candidate.parts:
        raise SandboxError(f"{label} must be a repository-relative path: {relative!r}")
    lexical = repo / Path(*candidate.parts)
    resolved = lexical.resolve()
    try:
        resolved.relative_to(repo)
    except ValueError as exc:
        raise SandboxError(f"{label} escapes its allowed repository root: {relative}") from exc
    if not resolved.exists():
        raise SandboxError(f"{label} does not exist: {relative}")
    return lexical, resolved


def _prepare_clone_target(
    clone: Path,
    relative: PurePosixPath,
    *,
    directory: bool | None,
    label: str,
) -> Path:
    """Validate/create a clone-local target without following any symlink.

    This runs before Bubblewrap, so ordinary ``Path.mkdir``/``touch`` calls
    are not acceptable: an untrusted tracked parent symlink could redirect a
    mount placeholder write into the host filesystem.
    """

View on GitHub (pinned to d540b00184)

Solutions

  1. Remove or replace the escaping symlink inside the repo with the real file/dir copied in.
  2. Validate repo contents for escaping symlinks before staging: reject any entry whose resolved target is not within repo.
  3. Re-clone the repo from a trusted source to drop planted symlinks.
  4. If the external target is legitimately needed, copy its contents into the repo rather than linking out.

Example fix

// before
# repo/asset -> /etc
src, resolved = _safe_repo_source(repo, 'asset', label='task asset')
// after
link = repo / 'asset'
if link.is_symlink():
    link.unlink()
shutil.copytree('/trusted/asset', link)
src, resolved = _safe_repo_source(repo, 'asset', label='task asset')
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path, PurePosixPath

def stays_within_repo(repo: Path, rel: str) -> bool:
    candidate = PurePosixPath(rel)
    if candidate.is_absolute() or '..' in candidate.parts or not candidate.parts:
        return False
    try:
        (repo / Path(*candidate.parts)).resolve().relative_to(repo)
        return True
    except ValueError:
        return False

assert stays_within_repo(repo, relative)

Type guard

from pathlib import Path, PurePosixPath

def resolves_within_repo(repo: Path, value: object) -> bool:
    if not isinstance(value, str):
        return False
    candidate = PurePosixPath(value)
    if candidate.is_absolute() or '..' in candidate.parts or not candidate.parts:
        return False
    try:
        (repo / Path(*candidate.parts)).resolve().relative_to(repo)
        return True
    except ValueError:
        return False

Try / catch

try:
    src, resolved = _safe_repo_source(repo, relative, label=label)
except SandboxError as exc:
    if 'escapes its allowed repository root' in str(exc):
        link = repo / relative
        if link.is_symlink():
            link.unlink()
            shutil.copytree(resolve_external_target(), link)
        src, resolved = _safe_repo_source(repo, relative, label=label)
    raise

Prevention

When it happens

Trigger: A repo-relative path with no '..' that nonetheless resolves outside repo because an intermediate component is a symlink to an external location, e.g. repo/asset where asset -> /etc. resolve().relative_to(repo) then raises ValueError.

Common situations: A symlink inside the workspace points outside (supply-chain or accidental); a repo contains a submodule or linked asset that resolves above the root; a mount/copy of assets introduced a symlink escaping the repo; a test fixture planted a symlink to shared storage; an attacker/proposer planted an escaping symlink as a traversal attempt.

Related errors


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