abhigyanpatwari/GitNexus · error · SandboxError

target-controlled {name} must not be a directory

Error message

target-controlled {name} must not be a directory

What it means

Precondition guard in _replace_control_file. Before overwriting .gitnexusrc or .gitnexusignore to neutralize target index inputs, the helper lstat's the path; if it currently exists as a directory, the harness refuses (it cannot atomically replace a directory with a regular file via O_CREAT|O_EXCL). This protects the neutralization step from an inconsistent starting state.

Source

Thrown at eval/workflow_bench/sanitized_graph.py:114

        raise SandboxError("sandbox_dependencies must be a list")
    for item in dependencies:
        if not isinstance(item, Mapping):
            continue
        for field in ("source", "target"):
            value = item.get(field)
            if isinstance(value, str) and _is_restricted_path(value):
                raise SandboxError(f"sandbox dependency cannot expose prebuilt graph or harness data: {value}")


def _replace_control_file(root: Path, name: str, payload: bytes) -> None:
    path = root / name
    try:
        metadata = path.lstat()
    except FileNotFoundError:
        metadata = None
    if metadata is not None:
        if stat.S_ISDIR(metadata.st_mode):
            raise SandboxError(f"target-controlled {name} must not be a directory")
        path.unlink()
    descriptor = os.open(
        path,
        os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
        0o600,
    )
    try:
        view = memoryview(payload)
        while view:
            written = os.write(descriptor, view)
            if written <= 0:
                raise OSError(f"short write while neutralizing {name}")
            view = view[written:]
        os.fsync(descriptor)
    finally:
        os.close(descriptor)

View on GitHub (pinned to d540b00184)

Solutions

  1. Inspect the clone root: 'ls -la <clone>/.gitnexusrc <clone>/.gitnexusignore' and confirm they are not directories.
  2. Remove or convert the directory to a regular file in the task fixture so the harness can replace it.
  3. Re-create the sanitized task snapshot after fixing the fixture so the bad state does not recur.
  4. Check that no sandbox_copy or sandbox_dependencies entry creates one of these names as a directory.

Example fix

# before (fixture builds .gitnexusrc as a dir)
mkdir .gitnexusrc
# after
echo '{}' > .gitnexusrc
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
import stat

clone = Path("clone-root")
for name in (".gitnexusrc", ".gitnexusignore"):
    p = clone / name
    if p.is_symlink():
        raise SystemExit(f"{name} is a symlink")
    if p.exists() and not p.is_file():
        raise SystemExit(f"{name} must be a regular file, not a directory")

Type guard

from pathlib import Path
import stat

def control_file_is_writable(path: Path) -> bool:
    try:
        mode = path.lstat().st_mode
    except FileNotFoundError:
        return True
    return not stat.S_ISDIR(mode) and not stat.S_ISLNK(mode)

Try / catch

# _replace_control_file is internal; pre-clean the clone before graph prep:
try:
    _neutralize_target_index_inputs(clone_root)
except SandboxError as exc:
    if "must not be a directory" in str(exc):
        log.error(".gitnexusrc/.gitnexusignore is a dir in the fixture; convert to a file")
    raise

Prevention

When it happens

Trigger: _neutralize_target_index_inputs calls _replace_control_file for .gitnexusrc or .gitnexusignore, and one of them is a directory in the sanitized clone root. The error message interpolates the offending control-file name.

Common situations: A task spec or fixture created .gitnexusrc/ as a directory (e.g. to hold snippets); a symlinked config layout resolved to a directory; a previous failed run left a partial directory behind.

Related errors


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