{"record":{"id":"c2bf6caa133b7b79","repo":"abhigyanpatwari/GitNexus","slug":"candidate-overlay-cannot-contain-symlinks-relati","errorCode":null,"errorMessage":"candidate overlay cannot contain symlinks: {relative}","messagePattern":"candidate overlay cannot contain symlinks: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"critical","filePath":"eval/workflow_bench/evolution.py","lineNumber":302,"sourceCode":"    entry_count = 0\n    while pending:\n        directory = pending.pop()\n        child_directories: list[Path] = []\n        try:\n            iterator = os.scandir(directory)\n        except OSError as exc:\n            raise ValueError(f\"candidate overlay directory is unreadable: {directory}: {exc}\") from exc\n        with iterator:\n            for item in iterator:\n                entry_count += 1\n                if entry_count > MAX_CANDIDATE_ENTRIES:\n                    raise ValueError(f\"candidate overlay exceeds the {MAX_CANDIDATE_ENTRIES}-entry limit\")\n                path = Path(item.path)\n                relative = path.relative_to(overlay)\n                if len(relative.as_posix().encode()) > MAX_CANDIDATE_PATH_BYTES:\n                    raise ValueError(f\"candidate overlay path exceeds {MAX_CANDIDATE_PATH_BYTES} bytes: {relative}\")\n                if item.is_symlink():\n                    raise ValueError(f\"candidate overlay cannot contain symlinks: {relative}\")\n                if item.is_dir(follow_symlinks=False):\n                    child_directories.append(path)\n                    continue\n                if not item.is_file(follow_symlinks=False):\n                    raise ValueError(f\"candidate overlay entries must be regular files: {relative}\")\n                entries.append(path)\n                if len(entries) > MAX_CANDIDATE_FILES:\n                    raise ValueError(f\"candidate overlay exceeds the {MAX_CANDIDATE_FILES}-file limit\")\n        pending.extend(child_directories)\n\n    entries.sort(key=lambda path: path.relative_to(overlay).as_posix())\n    if not entries:\n        raise ValueError(f\"candidate overlay contains no files: {overlay}\")\n\n    for path in entries:\n        relative = path.relative_to(overlay)\n        parts = relative.parts\n        if (","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/evolution.py#L284-L320","documentation":"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.","triggerScenarios":"Any symbolic link anywhere in the overlay: a symlinked file, a symlinked subdirectory, or a symlink whose target is outside the overlay root.","commonSituations":"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.","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."],"exampleFix":"# before: symlinked skill file\noverlay/.claude/skills/gitnexus-work/SKILL.md -> ../../shared/SKILL.md\n\n# after: materialize real copies\nimport shutil\nshutil.copytree('overlay', 'overlay-real', dirs_exist_ok=False, copy_function=shutil.copy2)\n# replace any link by copying its target bytes, then verify:\nfrom pathlib import Path\nassert not any(p.is_symlink() for p in Path('overlay-real').rglob('*'))","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef overlay_has_no_symlinks(root: Path) -> bool:\n    return not any(p.is_symlink() for p in Path(root).rglob('*'))","typeGuard":"from pathlib import Path\n\ndef is_symlink_free_tree(root: Path) -> bool:\n    return not any(p.is_symlink() for p in Path(root).rglob('*'))","tryCatchPattern":"try:\n    apply_candidate_overlay(overlay, worktree, sandbox=sandbox)\nexcept ValueError as exc:\n    if 'cannot contain symlinks' in str(exc):\n        # materialize symlink targets into real files, then retry\n        ...","preventionTips":["Copy overlay content with symlinks materialized (follow_symlinks).","Audit with any(p.is_symlink() ...) before running.","Never symlink skill files from shared directories."],"tags":["symlink","security","overlay","trust-boundary"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}