abhigyanpatwari/GitNexus · error · SandboxError

dependency symlink changed while snapshotting: {relative}

Error message

dependency symlink changed while snapshotting: {relative}

What it means

Raised by _copy_symlink after creating the destination link: if a re-stat of the source link shows a different mutation identity (dev, ino, mode, size, mtime_ns, ctime_ns) or a second readlink returns a different target string, the snapshot is rejected as inconsistent. This is the symlink analogue of the file-changed TOCTOU guard and guarantees the captured link target matches what was on disk at the recorded moment.

Source

Thrown at eval/workflow_bench/task_assets.py:490

            raise SandboxError(f"dependency symlink is unreadable or not UTF-8: {relative}") from exc
        if not target or PurePosixPath(target).is_absolute() or "\x00" in target:
            raise SandboxError(f"dependency symlink must be a bounded relative link: {relative}")
        if len(target_bytes) > MAX_TASK_ASSET_PATH_BYTES:
            raise SandboxError(f"dependency symlink target exceeds the path limit: {relative}")
        if self.budget.total_bytes + len(target_bytes) > MAX_TASK_ASSET_BYTES:
            raise SandboxError("sandbox_copy exceeds the total byte limit")
        destination = self.destination / Path(*relative.parts)
        os.symlink(target, destination)
        after = os.stat(name, dir_fd=parent_descriptor, follow_symlinks=False)
        if (
            _mutation_identity(before) != _mutation_identity(after)
            or os.readlink(
                name,
                dir_fd=parent_descriptor,
            )
            != target
        ):
            raise SandboxError(f"dependency symlink changed while snapshotting: {relative}")
        self.total_bytes += len(target_bytes)
        self.budget.total_bytes += len(target_bytes)
        self._record(
            AssetManifestEntry(
                path=relative,
                kind="symlink",
                size=len(target_bytes),
                sha256=hashlib.sha256(target_bytes).hexdigest(),
                link_target=target,
            )
        )

    def _ensure_parents(self, relative: PurePosixPath) -> None:
        current = PurePosixPath()
        for part in relative.parts:
            current /= part
            existing = self.entries.get(current)
            if existing is not None:

View on GitHub (pinned to d540b00184)

Solutions

  1. Quiesce the tree before capture: stop package managers, watch modes, and any process that may touch node_modules.
  2. Capture against a clean checkout — `git worktree add` at the resolved SHA and install dependencies into that isolated tree before snapshotting.
  3. Re-run after the tree is stable; this is a transient race.
  4. If reproducible, identify which process rewrites the link and sequence it out of the capture window.

Example fix

# before — capturing while pnpm deduplicates
pnpm install &
snapshot = cache.prepare(task, repo=repo, resolved_sha=sha)

# after — finish install, then capture
pnpm install
wait
snapshot = cache.prepare(task, repo=repo, resolved_sha=sha)
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
import os, time

def assert_dep_symlinks_stable(dep_source: Path, gap: float = 0.5) -> None:
    def fp():
        out = {}
        for current, dirs, files in os.walk(dep_source, followlinks=False):
            for name in dirs + files:
                p = Path(current) / name
                if p.is_symlink():
                    st = p.lstat()
                    out[str(p)] = (st.st_ino, st.st_mtime_ns, os.readlink(p))
        return out
    a = fp(); time.sleep(gap); b = fp()
    if a != b:
        raise ValueError(f"dependency symlinks not stable: {[k for k in a if a[k]!=b.get(k)][:5]}")

for d in task.get("sandbox_dependencies", []):
    assert_dep_symlinks_stable(repo_path / d["source"])

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):
        # stop package managers / watch modes, then retry once
        raise
    raise

Prevention

When it happens

Trigger: The symlink is deleted and recreated (changing ino), retargeted (`ln -sfn` by another process), or its metadata changed (chmod) between the initial stat (passed as `before`) and the post-copy verification. Most common when a package manager or build tool rewrites node_modules links concurrently with snapshot capture.

Common situations: npm/pnpm/yarn reinstalling or deduplicating links while the benchmark captures a dependency snapshot. A watch mode tool (vite, nodemon) touching node_modules. Running snapshot capture against a working tree another process is mutating.

Related errors


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