abhigyanpatwari/GitNexus · error · ValueError
{label} must be a real non-symlink directory: {path}
Error message
{label} must be a real non-symlink directory: {path} What it means
Thrown by _require_real_directory() in eval/workflow_bench/evolution.py when lstat() succeeds but the entry is a symlink (S_ISLNK) or not a directory (not S_ISDIR). The skill/promotion pipeline refuses symlinks and non-directories because they could redirect outside the sandbox, and refuses to follow links (no resolve()) to avoid erasing link semantics.
Source
Thrown at eval/workflow_bench/evolution.py:71
"subagents), or sum usage from the digest-bound transcript_artifacts in "
"each run output, deduplicating events "
"that share one message.id."
)
EVIDENCE_MAX_AGE_DAYS = 90
MAX_CANDIDATE_OVERLAY_BYTES = 4 * 1024 * 1024
MAX_SKILL_FINGERPRINT_BYTES = 4 * 1024 * 1024
MAX_CANDIDATE_ENTRIES = 256
MAX_CANDIDATE_FILES = 64
MAX_CANDIDATE_PATH_BYTES = 512
def _require_real_directory(path: Path, *, label: str) -> None:
try:
metadata = path.lstat()
except OSError as exc:
raise ValueError(f"{label} is unavailable: {path}: {exc}") from exc
if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
raise ValueError(f"{label} must be a real non-symlink directory: {path}")
def _require_directory_chain(root: Path, relative: Path, *, label: str) -> None:
"""Validate each lexical directory without erasing links via resolve()."""
_require_real_directory(root, label=label)
current = root
for part in relative.parts:
if part in {"", ".", ".."}:
raise ValueError(f"{label} contains an unsafe path component: {relative}")
current /= part
_require_real_directory(current, label=label)
def _bounded_regular_bytes(path: Path, *, limit: int, label: str) -> bytes:
"""Read one bounded regular file without following its leaf link."""
try:View on GitHub (pinned to d540b00184)
Solutions
- Replace the symlink with the real directory: `cp -rL` to dereference, or point directly at the target directory.
- Re-extract any tarball with --no-same-owner and dereference, or filter symlinks out of the candidate set.
- Point the argument at the directory itself, not a symlink or a file inside it.
- Audit the candidate-generation step to ensure it writes real directories.
Example fix
# before
ln -s /home/user/real-overlay /tmp/overlay
candidate_overlay_payload(Path('/tmp/overlay'))
# ValueError: candidate overlay directory must be a real non-symlink directory
# after
cp -rL /home/user/real-overlay /tmp/overlay
candidate_overlay_payload(Path('/tmp/overlay')) Defensive patterns
Strategy: validation
Validate before calling
import os, stat
def ensure_real_dir(p: Path, label: str) -> None:
st = p.lstat()
if stat.S_ISLNK(st.st_mode) or not stat.S_ISDIR(st.st_mode):
raise SystemExit(f'{label} must be a real directory, not a symlink/file: {p}')
# call before _require_real_directory Type guard
def is_not_real_directory_error(exc: ValueError) -> bool:
return 'must be a real non-symlink directory' in str(exc) Try / catch
try:
candidate_overlay_payload(overlay)
except ValueError as e:
if is_not_real_directory_error(e):
# re-stage with cp -rL to dereference, then retry
raise
raise Prevention
- Never pass a symlink as the overlay root; dereference with cp -rL first.
- Ensure tarball extraction does not preserve symlinks.
- Point the argument at a directory, never at a file.
When it happens
Trigger: An overlay or skill-directory path that is actually a symlink (e.g. `ln -s /elsewhere /tmp/overlay`), or a path that points at a regular file rather than a directory. _require_directory_chain calls this for the root and for each lexical path component.
Common situations: User created a convenience symlink to their candidate overlay; a tool packed the overlay as a tarball that extracted symlinks; path points at a file (e.g. a yaml) instead of its parent dir; CI set up a symlinked workspace.
Related errors
- {label} must be a regular non-symlink file: {path}
- {label} is unavailable: {path}: {exc}
- {label} contains an unsafe path component: {relative}
- {label} is unreadable: {path}: {exc}
- {label} changed while opening: {path}
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/601b9353f0908158.
Report an issue: GitHub.