abhigyanpatwari/GitNexus · critical · ValueError

candidate overlay cannot traverse symlinks: {overlay}

Error message

candidate overlay cannot traverse symlinks: {overlay}

What it means

Thrown by candidate_overlay_files (evolution.py:279) when the resolved overlay path differs from the lexical absolute path. The harness requires the overlay root to be a real path that does not traverse any symlink, because resolve() following a symlink would hide a redirect outside the trusted tree. Any symlink component in the supplied path triggers this.

Source

Thrown at eval/workflow_bench/evolution.py:279

        env=build_sandbox_environment(),
    )
    return command, result


def candidate_overlay_files(overlay: Path) -> list[Path]:
    """Return a candidate's files after enforcing the benchmark trust boundary.

    Candidates may change only the canonical repo-local skill prompts. They
    cannot modify task code, tests, or verification commands and thereby game
    the promotion gate.
    """
    overlay = overlay.expanduser().absolute()
    try:
        resolved_overlay = overlay.resolve(strict=True)
    except OSError as exc:
        raise ValueError(f"candidate overlay is not a directory: {overlay}") from exc
    if resolved_overlay != overlay:
        raise ValueError(f"candidate overlay cannot traverse symlinks: {overlay}")
    _require_real_directory(overlay, label="candidate overlay")

    entries: list[Path] = []
    pending = [overlay]
    entry_count = 0
    while pending:
        directory = pending.pop()
        child_directories: list[Path] = []
        try:
            iterator = os.scandir(directory)
        except OSError as exc:
            raise ValueError(f"candidate overlay directory is unreadable: {directory}: {exc}") from exc
        with iterator:
            for item in iterator:
                entry_count += 1
                if entry_count > MAX_CANDIDATE_ENTRIES:
                    raise ValueError(f"candidate overlay exceeds the {MAX_CANDIDATE_ENTRIES}-entry limit")
                path = Path(item.path)

View on GitHub (pinned to d540b00184)

Solutions

  1. Pass a real (non-symlink) absolute path; resolve the symlink yourself and pass the resolved real path if it is itself real.
  2. Copy the overlay into a fresh real directory (shutil.copytree into tempfile.mkdtemp) and pass that.
  3. On macOS use /private/tmp/... directly instead of /tmp/...; avoid symlinked project roots.

Example fix

# before: overlay root is a symlink (e.g. macOS /tmp)
apply_candidate_overlay(Path('/tmp/overlay'), ...)  # /tmp -> /private/tmp

# after: copy to a real path with no symlink components
import tempfile, shutil
real = Path(tempfile.mkdtemp(prefix='wfbench-overlay-'))
shutil.copytree('/tmp/overlay', real / 'overlay')
assert (real / 'overlay').resolve() == (real / 'overlay')  # no symlink hop
apply_candidate_overlay(real / 'overlay', ...)
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

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

Type guard

null

Try / catch

try:
    apply_candidate_overlay(overlay, worktree, sandbox=sandbox)
except ValueError as exc:
    if 'cannot traverse symlinks' in str(exc):
        # copy overlay to a real path (no symlink components) and retry
        ...

Prevention

When it happens

Trigger: The overlay path contains a symlink component (the root itself is a symlink, or a parent like /tmp is a symlink). resolve() lands on a different path than the lexical absolute input, so the equality check fails.

Common situations: On macOS /tmp -> /private/tmp and /var -> /private/var; pointing at a symlinked project checkout; a CI workspace that symlinks into another volume.

Related errors


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