abhigyanpatwari/GitNexus · error · SandboxError

{label} must not traverse symlinks: {lexical}

Error message

{label} must not traverse symlinks: {lexical}

What it means

Raised by _real_directory when strict resolution succeeds but resolved != lexical — meaning a symlink somewhere in the path caused the resolved absolute path to differ from the lexical absolute path. The contract requires the path itself be canonical (no symlink hops), so even a valid target reached through a symlink is rejected.

Source

Thrown at eval/workflow_bench/proposer_sandbox.py:557

    _resolve_executable(None, "socat")


def _real_directory(path: Path, *, label: str) -> Path:
    """Return an absolute directory path without accepting any symlink hop."""

    lexical = path.expanduser().absolute()
    try:
        mode = lexical.lstat().st_mode
    except OSError as exc:
        raise SandboxError(f"{label} must be a real directory: {lexical}: {exc}") from exc
    if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode):
        raise SandboxError(f"{label} must be a real directory: {lexical}")
    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

View on GitHub (pinned to d540b00184)

Solutions

  1. Replace the symlink with the real directory at the lexical path: remove the symlink, then move/bind-mount the target into place, or update the config to the resolved canonical path.
  2. If the contract is too strict for your legit setup, point the config value at the resolved canonical path so lexical == resolved.
  3. Use a bind mount (mount --bind) instead of a symlink so the lexical path is the real directory.
  4. Repackage the asset without the symlink indirection.

Example fix

// before
# /opt/claude -> /mnt/claude-1.2
real = _real_directory(Path('/opt/claude'), label='claude root')
// after
real = _real_directory(Path('/mnt/claude-1.2'), label='claude root')
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def is_canonical_dir(p: Path) -> bool:
    lexical = p.expanduser().absolute()
    try:
        resolved = lexical.resolve(strict=True)
    except OSError:
        return False
    return resolved == lexical

assert is_canonical_dir(candidate)

Type guard

from pathlib import Path

def is_canonical_real_directory(value: object) -> bool:
    if not isinstance(value, Path):
        return False
    lexical = value.expanduser().absolute()
    try:
        resolved = lexical.resolve(strict=True)
    except OSError:
        return False
    return resolved == lexical

Try / catch

try:
    real = _real_directory(path, label=label)
except SandboxError as exc:
    if 'must not traverse symlinks' in str(exc):
        canonical = path.resolve(strict=True)
        raise SystemExit(f'point config at canonical path: {canonical}')
    raise

Prevention

When it happens

Trigger: The lexical path is absolute and a real directory, but one of its components (or the leaf) is a symlink, so Path.resolve(strict=True) returns a different absolute path. e.g. lexical /opt/claude where /opt/claude -> /mnt/x, resolved becomes /mnt/x.

Common situations: Operator symlinked a directory to another mount for space (/var/lib/foo -> /data/foo); a 'convenience' symlink in /opt or /home/agent; a distro packaged the dir as a symlink to a versioned path; a CI image layered symlinks to share assets; user pointed HOME-ish paths through /usr/local equivalence.

Related errors


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