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
- Quiesce the source repo for the duration of prepare (no writers).
- Capture from a read-only checkout or a git worktree dedicated to snapshotting.
- 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
- Quiesce the source repo (no writers) for the duration of TaskAssetCache.prepare.
- Capture from a read-only checkout or a dedicated git worktree.
- Retry prepare after stopping any concurrent writer.
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
- sanitized graph source changed while opening: {relative}
- sanitized graph source changed while scanning: {relative}
- {label} is unreadable: {path}: {exc}
- {label} changed while opening: {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/b68bb939429ce536.
Report an issue: GitHub.