abhigyanpatwari/GitNexus · error · SandboxError
sandbox_copy file changed while snapshotting: {relative}
Error message
sandbox_copy file changed while snapshotting: {relative} What it means
Raised by _copy_file after the read loop completes: if the bytes actually copied differ from the st_size recorded before the copy, or if the file's mutation identity (dev, ino, mode, size, mtime_ns, ctime_ns) changed between the pre-copy fstat and a post-copy fstat, the snapshot is rejected as inconsistent. This is a TOCTOU guard guaranteeing the captured bytes match the recorded manifest exactly.
Source
Thrown at eval/workflow_bench/task_assets.py:448
copied = 0
try:
while True:
chunk = _read_source_chunk(descriptor, COPY_CHUNK_BYTES)
if not chunk:
break
copied += len(chunk)
if self.budget.total_bytes + copied > MAX_TASK_ASSET_BYTES:
raise SandboxError("sandbox_copy exceeds the total byte limit")
digest.update(chunk)
_write_all(output, chunk)
captured_mode = stat.S_IMODE(before.st_mode) if self.preserve_modes else 0
frozen_mode = 0o400 | (0o100 if self.preserve_modes and captured_mode & 0o111 else 0)
os.fchmod(output, frozen_mode)
finally:
os.close(output)
after = os.fstat(descriptor)
if copied != before.st_size or _mutation_identity(before) != _mutation_identity(after):
raise SandboxError(f"sandbox_copy file changed while snapshotting: {relative}")
self.total_bytes += copied
self.budget.total_bytes += copied
self._record(
AssetManifestEntry(
path=relative,
kind="file",
size=copied,
sha256=digest.hexdigest(),
mode=captured_mode,
)
)
def _copy_symlink(
self,
parent_descriptor: int,
name: str,
relative: PurePosixPath,
before: os.stat_result,View on GitHub (pinned to d540b00184)
Solutions
- Capture the snapshot against a clean, quiescent checkout of resolved_sha — stop all writers (indexers, watchers, formatters) before prepare().
- Use `git stash` or a fresh `git worktree add` at the resolved SHA so no unrelated process can mutate the tree.
- If a daemon is unavoidable, pause it for the duration of TaskAssetCache.prepare and resume after.
- Re-run once the working tree is stable; this is a transient race, not a declaration bug.
Example fix
# before — snapshotting a tree a formatter is touching black --check repo/ & # may rewrite files snapshot = cache.prepare(task, repo=repo, resolved_sha=sha) # after — quiesce first wait $(jobs -p) worktree=$(mktemp -d) git -C "$repo" worktree add "$worktree" "$sha" snapshot = cache.prepare(task, repo=Path(worktree), resolved_sha=sha)
Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
import os, time
def assert_tree_stable(repo: Path, declarations: list[str], gap: float = 0.5) -> None:
def fingerprint():
fp = {}
for raw in declarations:
for current, _, files in os.walk(repo / raw, followlinks=False):
for name in files:
p = Path(current) / name
st = p.lstat()
fp[str(p)] = (st.st_ino, st.st_size, st.st_mtime_ns, st.st_ctime_ns)
return fp
a = fingerprint()
time.sleep(gap)
b = fingerprint()
if a != b:
changed = [k for k in a if a.get(k) != b.get(k)]
raise ValueError(f"tree not stable before snapshot: {changed[:5]}")
assert_tree_stable(repo_path, task["sandbox_copy"]) Try / catch
from eval.workflow_bench.propposer_sandbox import SandboxError
try:
snapshot = cache.prepare(task, repo=repo, resolved_sha=sha)
except SandboxError as exc:
if "changed while snapshotting" in str(exc):
# transient race — quiesce writers and retry once; do NOT retry unchanged
raise
raise Prevention
- Snapshot a dedicated clean worktree at resolved_sha rather than a live working tree.
- Stop formatters, linters, indexers, and watch modes for the duration of prepare.
- Treat 'changed while snapshotting' as a signal of concurrent mutation, not a declaration bug.
When it happens
Trigger: Any concurrent mutation of the source file between the initial fstat (captured in copy_descriptor as `before`) and the post-copy fstat: truncation, append, rewrite via atomic replace (which changes ino), or a chmod that alters mode. The check `copied != before.st_size` also catches a short read that ended early without the file growing.
Common situations: A build tool rewrites a config file mid-snapshot. A linter formats a source file. An indexer updates a database file. git operations (checkout, reset) touch files. Running the snapshot against a live working tree instead of a clean checked-out revision.
Related errors
- dependency symlink changed while snapshotting: {relative}
- oracle stage root changed during verification
- oracle file changed during verification: {item.target}
- overlay destination changed while being read: {target}
- repository root changed during overlay {phase}: {root}
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/012a04b9d133d56e.
Report an issue: GitHub.