abhigyanpatwari/GitNexus · error · ValueError
repository root must be a real directory: {root}
Error message
repository root must be a real directory: {root} What it means
All errors below are raised by internal helpers of `eval/workflow_bench/promotion_apply.py` and propagate to the caller of the public entry points: `apply_promoted_overlay(overlay, repo_root, *, expected_digest, expected_target_bases)`, `destination_base_digests(overlay, repo_root)`, `committed_destination_base_digests(overlay, repo_root, *, ref)` and `freeze_overlay(overlay, destination)`. The module applies promoted skill overlays across the canonical skill tree plus its shipped mirrors (`gitnexus/skills`, `gitnexus-claude-plugin/skills`) in a TOCTOU-hardened, symlink-rejecting, descriptor-bound transaction. After `lstat`/`resolve` succeed, `_open_repository_root` checks `stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode)`. A symlink-to-a-dir, a regular file, a socket, or any non-directory trips the guard. The repository root must be a literal on-disk directory.
Source
Thrown at eval/workflow_bench/promotion_apply.py:158
if (
stat.S_ISLNK(final.st_mode)
or not stat.S_ISREG(final.st_mode)
or not (identity(before) == identity(opened) == identity(after) == identity(final))
):
raise ValueError(f"overlay destination changed while being read: {target}")
return b"".join(chunks), opened.st_mode
def _open_repository_root(repo_root: Path) -> tuple[Path, int]:
root = repo_root.expanduser().absolute()
try:
metadata = root.lstat()
resolved = root.resolve(strict=True)
except OSError as exc:
raise ValueError(f"repository root is unavailable: {root}") from exc
if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
raise ValueError(f"repository root must be a real directory: {root}")
if resolved != root:
raise ValueError(f"repository root must not traverse symlinks: {root}")
flags = os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
try:
descriptor = os.open(root, flags)
except OSError as exc:
raise ValueError(f"repository root changed while opening: {root}") from exc
try:
opened = os.fstat(descriptor)
final = root.lstat()
final_resolved = root.resolve(strict=True)
def identity(value: os.stat_result) -> tuple[int, int, int]:
return value.st_dev, value.st_ino, stat.S_IFMT(value.st_mode)
if (
stat.S_ISLNK(final.st_mode)
or not stat.S_ISDIR(opened.st_mode)View on GitHub (pinned to d540b00184)
Solutions
- Inspect the entry: `pathlib.Path(root).lstat()` and confirm `S_ISDIR` and not `S_ISLNK`.
- Replace any symlink with a real directory (`readlink` then bind-mount or move).
- Re-derive and pass the real checkout directory explicitly.
Example fix
// before
root = Path('/repo') # /repo -> /data/checkout (symlink)
apply_promoted_overlay(overlay, repo_root=root)
// after
root = Path('/data/checkout') # the real directory
assert root.is_dir() and not root.is_symlink()
apply_promoted_overlay(overlay, repo_root=root) Defensive patterns
Strategy: validation
Validate before calling
def assert_real_dir(p):
root = Path(p).expanduser().absolute()
st = root.lstat()
if stat.S_ISLNK(st.st_mode) or not stat.S_ISDIR(st.st_mode):
raise ValueError(f'repo_root must be a real directory, got: {root}')
return root Type guard
import stat
def is_real_directory(p: str) -> bool:
root = Path(p)
try:
st = root.lstat()
except OSError:
return False
return (not stat.S_ISLNK(st.st_mode)) and stat.S_ISDIR(st.st_mode) Try / catch
except ValueError as exc:
if 'must be a real directory' in str(exc):
raise SystemExit(f'repo_root is a symlink or non-directory: {exc}') Prevention
- Never symlink the repository root; promotion rejects symlinks by design.
- In CI, clone into a fresh real directory rather than a symlinked cache path.
- Unit-test `is_real_directory(repo_root)` in your harness before applying overlays.
When it happens
Trigger: Pointing `repo_root` at a symlink (even one targeting a real dir), a regular file, or any non-directory filesystem entry.
Common situations: Operator symlinked the checkout for convenience; `repo_root` accidentally resolves to a tarball/sparse-file; default `REPO_ROOT` miscomputed via `parents[N]` after a file move.
Related errors
- repository root must not traverse symlinks: {root}
- overlay destination parent must not be a symlink: {target}
- {label} must be a real directory: {lexical}: {exc}
- {label} must be a real directory: {lexical}
- {label} must not traverse symlinks: {lexical}
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/375c9f6b5b4f9bdc.
Report an issue: GitHub.