abhigyanpatwari/GitNexus · error · ValueError
plan artifact changed while hashing: {path}
Error message
plan artifact changed while hashing: {path} What it means
Thrown by snapshot_plan_docs after it has streamed the whole plan file through sha256. Once the digest is computed the descriptor is fstat'd again; if st_size or st_mtime_ns changed between the pre-read fstat and this post-read fstat, the file was modified while being hashed, so the digest does not describe a stable artifact. The harness rejects it to keep evidence reproducible.
Source
Thrown at eval/workflow_bench/runner_artifacts.py:281
snapshot: dict[Path, str] = {}
for path in sorted(plans.iterdir()):
if path.suffix.lower() not in {".md", ".html"}:
continue
metadata = path.lstat()
if stat.S_ISLNK(metadata.st_mode):
raise ValueError(f"plan artifact cannot be a symlink: {path}")
if not stat.S_ISREG(metadata.st_mode):
raise ValueError(f"plan artifact must be a regular file: {path}")
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
try:
opened = os.fstat(descriptor)
if not stat.S_ISREG(opened.st_mode) or opened.st_dev != metadata.st_dev or opened.st_ino != metadata.st_ino:
raise ValueError(f"plan artifact changed while opening: {path}")
with os.fdopen(descriptor, "rb", closefd=False) as handle:
snapshot[path] = hashlib.file_digest(handle, "sha256").hexdigest()
after = os.fstat(descriptor)
if (opened.st_size, opened.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
raise ValueError(f"plan artifact changed while hashing: {path}")
finally:
os.close(descriptor)
return snapshot
def new_plan_doc(worktree: Path, before: dict[Path, str]) -> Path:
"""Return the sole new or modified plan, rejecting ambiguous evidence."""
after = snapshot_plan_docs(worktree)
deleted = sorted(path for path in before if path not in after)
if deleted:
raise ValueError("planning deleted existing plan artifact(s): " + ", ".join(str(path) for path in deleted))
changed = sorted(path for path, digest in after.items() if before.get(path) != digest)
if len(changed) != 1:
raise ValueError(f"planning must create or modify exactly one plan artifact; observed {len(changed)}")
return changed[0]
View on GitHub (pinned to d540b00184)
Solutions
- Guarantee the agent process is fully terminated before snapshot_plan_docs runs.
- Disable any file-sync or auto-format watcher on the worktree's docs/plans directory.
- Move the worktree off any FUSE/network filesystem onto local disk.
- Retry the arm once the writing process is confirmed stopped (check no process holds the file open with lsof).
Example fix
// before agent_proc.terminate() # snapshot may race with flush snapshot = snapshot_plan_docs(worktree) // after agent_proc.wait() # ensure all writes flushed and fd closed snapshot = snapshot_plan_docs(worktree)
Defensive patterns
Strategy: validation
Validate before calling
import os, stat, time
from pathlib import Path
def is_stable_during_read(path: Path) -> bool:
st_before = path.stat()
with open(path, 'rb') as fh:
fh.read()
st_after = path.stat()
return (st_before.st_size, st_before.st_mtime_ns) == (st_after.st_size, st_after.st_mtime_ns) Type guard
null
Try / catch
try:
snap = snapshot_plan_docs(worktree)
except ValueError as e:
if 'changed while hashing' in str(e):
# file modified during read; ensure writer is stopped and retry once
raise SystemExit('plan modified during hashing; stop the writer and retry')
raise Prevention
- Confirm no editor, linter, or sync daemon touches docs/plans during the run.
- Take the snapshot only after the producing process has exited.
- Prefer local disk over FUSE/network filesystems for the worktree.
- Log st_mtime_ns before and after if races recur, to identify the writer.
When it happens
Trigger: (opened.st_size, opened.st_mtime_ns) != (after.st_size, after.st_mtime_ns) after hashlib.file_digest completes. The plan file's size or nanosecond mtime shifted during the read loop.
Common situations: An agent still writing its plan when the snapshot runs; an editor/linter auto-formatting the file on a timer; a sync daemon (Dropbox/iCloud) touching the file; a filesystem that updates mtime on read (rare, some FUSE setups).
Related errors
- plan artifact changed while opening: {path}
- oracle stage root changed during verification
- oracle file changed during verification: {item.target}
- repository root changed during overlay {phase}: {root}
- overlay destination parent changed during {phase}: {item['ta
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/aac414a1de97ec55.
Report an issue: GitHub.