abhigyanpatwari/GitNexus · error · SandboxError

sandbox_copy directory changed while snapshotting: {relative

Error message

sandbox_copy directory changed while snapshotting: {relative}

What it means

TOCTOU guard in the recursive copier: after listing and descending into children, a final os.fstat of the directory differs in _mutation_identity (dev, ino, mode, size, mtime_ns, ctime_ns) from the value taken before the listing. The directory was mutated during capture.

Source

Thrown at eval/workflow_bench/task_assets.py:410

                names = sorted(os.listdir(descriptor))
            except OSError as exc:
                raise SandboxError(f"sandbox_copy directory is unreadable: {relative}: {exc}") from exc
            for name in names:
                child_relative = relative / name
                child_metadata = os.stat(name, dir_fd=descriptor, follow_symlinks=False)
                if stat.S_ISLNK(child_metadata.st_mode):
                    if not self.allow_symlinks:
                        raise SandboxError(f"sandbox_copy must not traverse a symlink: {child_relative}")
                    self._copy_symlink(descriptor, name, child_relative, child_metadata)
                    continue
                child = _open_child(descriptor, name, child_relative)
                try:
                    self.copy_descriptor(child, child_relative)
                finally:
                    os.close(child)
            after = os.fstat(descriptor)
            if _mutation_identity(before) != _mutation_identity(after):
                raise SandboxError(f"sandbox_copy directory changed while snapshotting: {relative}")
            return
        if not stat.S_ISREG(before.st_mode):
            raise SandboxError(f"sandbox_copy accepts only regular files and directories: {relative}")
        self._copy_file(descriptor, relative, before)

    def _record_directory(self, relative: PurePosixPath) -> None:
        self._ensure_parents(relative.parent)
        self._record(AssetManifestEntry(path=relative, kind="directory"))
        destination = self.destination / Path(*relative.parts)
        destination.mkdir(mode=0o700, exist_ok=True)

    def _copy_file(self, descriptor: int, relative: PurePosixPath, before: os.stat_result) -> None:
        self._ensure_parents(relative.parent)
        if self.budget.total_bytes + before.st_size > MAX_TASK_ASSET_BYTES:
            raise SandboxError("sandbox_copy exceeds the total byte limit")
        destination = self.destination / Path(*relative.parts)
        flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0)
        output = os.open(destination, flags, 0o600)

View on GitHub (pinned to d540b00184)

Solutions

  1. Quiesce the source repo for the duration of prepare (no writers).
  2. Capture from a read-only checkout or a git worktree dedicated to snapshotting.
  3. Retry prepare after the writer stops.
Defensive patterns

Strategy: validation

Validate before calling

import fcntl, os

def lock_source_tree(repo):
    """Hold an exclusive flock on the repo root for the duration of prepare."""
    fd = os.open(repo, os.O_RDONLY | os.O_DIRECTORY)
    fcntl.flock(fd, fcntl.LOCK_EX)
    return fd

Prevention

When it happens

Trigger: Concurrent modification of a sandbox_copy source directory (entries added/removed/renamed/mode-changed) while TaskAssetCache.prepare is recursing through it.

Common situations: A build/install writing into the source tree during snapshot; git operations on the repo; an indexer/IDE touching files; a shared worktree across arms.

Related errors


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