abhigyanpatwari/GitNexus · error · SandboxError
{label} must not traverse symlinks: {lexical}
Error message
{label} must not traverse symlinks: {lexical} What it means
Raised by _real_directory when strict resolution succeeds but resolved != lexical — meaning a symlink somewhere in the path caused the resolved absolute path to differ from the lexical absolute path. The contract requires the path itself be canonical (no symlink hops), so even a valid target reached through a symlink is rejected.
Source
Thrown at eval/workflow_bench/proposer_sandbox.py:557
_resolve_executable(None, "socat")
def _real_directory(path: Path, *, label: str) -> Path:
"""Return an absolute directory path without accepting any symlink hop."""
lexical = path.expanduser().absolute()
try:
mode = lexical.lstat().st_mode
except OSError as exc:
raise SandboxError(f"{label} must be a real directory: {lexical}: {exc}") from exc
if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode):
raise SandboxError(f"{label} must be a real directory: {lexical}")
try:
resolved = lexical.resolve(strict=True)
except OSError as exc:
raise SandboxError(f"{label} must be a real directory: {lexical}: {exc}") from exc
if resolved != lexical:
raise SandboxError(f"{label} must not traverse symlinks: {lexical}")
return lexical
def _safe_repo_source(repo: Path, relative: str, *, label: str) -> tuple[Path, Path]:
candidate = PurePosixPath(relative)
if candidate.is_absolute() or ".." in candidate.parts or not candidate.parts:
raise SandboxError(f"{label} must be a repository-relative path: {relative!r}")
lexical = repo / Path(*candidate.parts)
resolved = lexical.resolve()
try:
resolved.relative_to(repo)
except ValueError as exc:
raise SandboxError(f"{label} escapes its allowed repository root: {relative}") from exc
if not resolved.exists():
raise SandboxError(f"{label} does not exist: {relative}")
return lexical, resolved
View on GitHub (pinned to d540b00184)
Solutions
- Replace the symlink with the real directory at the lexical path: remove the symlink, then move/bind-mount the target into place, or update the config to the resolved canonical path.
- If the contract is too strict for your legit setup, point the config value at the resolved canonical path so lexical == resolved.
- Use a bind mount (mount --bind) instead of a symlink so the lexical path is the real directory.
- Repackage the asset without the symlink indirection.
Example fix
// before
# /opt/claude -> /mnt/claude-1.2
real = _real_directory(Path('/opt/claude'), label='claude root')
// after
real = _real_directory(Path('/mnt/claude-1.2'), label='claude root') Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def is_canonical_dir(p: Path) -> bool:
lexical = p.expanduser().absolute()
try:
resolved = lexical.resolve(strict=True)
except OSError:
return False
return resolved == lexical
assert is_canonical_dir(candidate) Type guard
from pathlib import Path
def is_canonical_real_directory(value: object) -> bool:
if not isinstance(value, Path):
return False
lexical = value.expanduser().absolute()
try:
resolved = lexical.resolve(strict=True)
except OSError:
return False
return resolved == lexical Try / catch
try:
real = _real_directory(path, label=label)
except SandboxError as exc:
if 'must not traverse symlinks' in str(exc):
canonical = path.resolve(strict=True)
raise SystemExit(f'point config at canonical path: {canonical}')
raise Prevention
- Point configs at canonical paths, not symlinks.
- Use bind mounts instead of symlinks.
- Periodically scan key directories for symlink components.
- Document the no-symlink-hop contract for operators.
When it happens
Trigger: The lexical path is absolute and a real directory, but one of its components (or the leaf) is a symlink, so Path.resolve(strict=True) returns a different absolute path. e.g. lexical /opt/claude where /opt/claude -> /mnt/x, resolved becomes /mnt/x.
Common situations: Operator symlinked a directory to another mount for space (/var/lib/foo -> /data/foo); a 'convenience' symlink in /opt or /home/agent; a distro packaged the dir as a symlink to a versioned path; a CI image layered symlinks to share assets; user pointed HOME-ish paths through /usr/local equivalence.
Related errors
- {label} must be a real directory: {lexical}: {exc}
- {label} must be a real directory: {lexical}
- {label} must be a real non-symlink directory: {path}
- {label} must be a regular non-symlink file: {path}
- evidence source must be a regular non-symlink file: {path}
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/da9b02a02e2b15f5.
Report an issue: GitHub.