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

  1. 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).
  2. Remove the symlink and use bind mounts or a real directory if the contract requires a non-symlink path.
  3. If a file was passed by mistake, pass its parent directory or the correct directory path.
  4. 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

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


AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12). Data as JSON: /api/errors/e5e5c8b62a5711dd. Report an issue: GitHub.