abhigyanpatwari/GitNexus · error · SandboxError
{label} must be a real directory: {lexical}
Error message
{label} must be a real directory: {lexical} What it means
Raised by _real_directory when lstat succeeds but the mode shows a symlink (S_ISLNK) or a non-directory (not S_ISDIR). The function's contract is to return a real directory with no symlink hop, so a symlink at the lexical path or any non-directory inode (regular file, device, socket) is rejected.
Source
Thrown at eval/workflow_bench/proposer_sandbox.py:551
]
def require_claude_sandbox_helpers() -> None:
"""Fail before paid work when Claude's mandatory inner sandbox cannot run."""
_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:View on GitHub (pinned to d540b00184)
Solutions
- Point directly at the real directory, not a symlink: pass the underlying target only after confirming it is a real dir (note _real_directory will still reject any symlink at the lexical position).
- Remove the symlink and use bind mounts or a real directory if the contract requires a non-symlink path.
- If a file was passed by mistake, pass its parent directory or the correct directory path.
- Recreate the path as a real directory (rmtree the symlink, mkdir).
Example fix
// before
real = _real_directory(Path('/opt/claude/shell-prefix'), label='shell prefix') # it's a symlink
// after
sp = Path('/opt/claude/shell-prefix')
if sp.is_symlink():
sp.unlink()
sp.mkdir(parents=True, mode=0o755)
real = _real_directory(sp, label='shell prefix') Defensive patterns
Strategy: validation
Validate before calling
import stat
from pathlib import Path
def is_real_non_symlink_dir(p: Path) -> bool:
p = p.expanduser().absolute()
try:
mode = p.lstat().st_mode
except OSError:
return False
return stat.S_ISDIR(mode) and not stat.S_ISLNK(mode)
assert is_real_non_symlink_dir(candidate) Type guard
import stat
from pathlib import Path
def is_real_directory_no_symlink(value: object) -> bool:
if not isinstance(value, Path):
return False
p = value.expanduser().absolute()
try:
mode = p.lstat().st_mode
except OSError:
return False
return stat.S_ISDIR(mode) and not stat.S_ISLNK(mode) Try / catch
try:
real = _real_directory(path, label=label)
except SandboxError as exc:
if 'must be a real directory' in str(exc):
if path.is_symlink():
raise SystemExit(f'{path} is a symlink; replace with a real directory')
raise
raise Prevention
- Avoid symlinks at directory config positions.
- Use bind mounts instead of symlinks where the contract forbids them.
- Pass real directories, not convenience symlinks.
- Add a setup check that key directories are real dirs.
When it happens
Trigger: The lexical path itself is a symlink (even if its target is a directory) or is not a directory at all — a regular file, a FIFO, a device, or a socket is passed where a directory is required.
Common situations: A 'directory' config value is actually a symlink for convenience; path points at a regular file (e.g. /opt/claude/claude binary instead of its parent dir); a mount source was replaced by a symlink to shared storage; a device or socket file occupies the path; operator symlinked /home/agent to /tmp/agent for space and the contract forbids it.
Related errors
- {label} must be a real directory: {lexical}: {exc}
- {label} must not traverse symlinks: {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/e5e5c8b62a5711dd.
Report an issue: GitHub.