abhigyanpatwari/GitNexus · critical · ValueError
candidate overlay cannot contain symlinks: {relative}
Error message
candidate overlay cannot contain symlinks: {relative} What it means
Thrown by candidate_overlay_files (evolution.py:302) when item.is_symlink() is True for any entry in the overlay tree. The trust boundary requires the overlay to contain only real files — no symlinks at all, anywhere — because a symlink could redirect content sourcing or staging outside the validated tree.
Source
Thrown at eval/workflow_bench/evolution.py:302
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)
relative = path.relative_to(overlay)
if len(relative.as_posix().encode()) > MAX_CANDIDATE_PATH_BYTES:
raise ValueError(f"candidate overlay path exceeds {MAX_CANDIDATE_PATH_BYTES} bytes: {relative}")
if item.is_symlink():
raise ValueError(f"candidate overlay cannot contain symlinks: {relative}")
if item.is_dir(follow_symlinks=False):
child_directories.append(path)
continue
if not item.is_file(follow_symlinks=False):
raise ValueError(f"candidate overlay entries must be regular files: {relative}")
entries.append(path)
if len(entries) > MAX_CANDIDATE_FILES:
raise ValueError(f"candidate overlay exceeds the {MAX_CANDIDATE_FILES}-file limit")
pending.extend(child_directories)
entries.sort(key=lambda path: path.relative_to(overlay).as_posix())
if not entries:
raise ValueError(f"candidate overlay contains no files: {overlay}")
for path in entries:
relative = path.relative_to(overlay)
parts = relative.parts
if (View on GitHub (pinned to d540b00184)
Solutions
- Replace every symlink with a real copy of its target file content.
- Rebuild the overlay with shutil.copytree(..., follow_symlinks) semantics so links are materialized.
- Remove leftover symlinks: find overlay -type l -delete, then add real files.
Example fix
# before: symlinked skill file
overlay/.claude/skills/gitnexus-work/SKILL.md -> ../../shared/SKILL.md
# after: materialize real copies
import shutil
shutil.copytree('overlay', 'overlay-real', dirs_exist_ok=False, copy_function=shutil.copy2)
# replace any link by copying its target bytes, then verify:
from pathlib import Path
assert not any(p.is_symlink() for p in Path('overlay-real').rglob('*')) Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def overlay_has_no_symlinks(root: Path) -> bool:
return not any(p.is_symlink() for p in Path(root).rglob('*')) Type guard
from pathlib import Path
def is_symlink_free_tree(root: Path) -> bool:
return not any(p.is_symlink() for p in Path(root).rglob('*')) Try / catch
try:
apply_candidate_overlay(overlay, worktree, sandbox=sandbox)
except ValueError as exc:
if 'cannot contain symlinks' in str(exc):
# materialize symlink targets into real files, then retry
... Prevention
- Copy overlay content with symlinks materialized (follow_symlinks).
- Audit with any(p.is_symlink() ...) before running.
- Never symlink skill files from shared directories.
When it happens
Trigger: Any symbolic link anywhere in the overlay: a symlinked file, a symlinked subdirectory, or a symlink whose target is outside the overlay root.
Common situations: Symlinking a shared SKILL.md from another project; a copy that preserved symlinks (cp -P instead of cp -L); a symlinked convenience directory inside the overlay.
Related errors
- candidate destination must be a regular non-symlink file: {r
- candidate overlay cannot traverse symlinks: {overlay}
- candidate destination escapes the clone: {relative}
- candidate overlay entries must be regular files: {relative}
- candidate overlays may only contain Markdown files under .cl
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/c2bf6caa133b7b79.
Report an issue: GitHub.