affaan-m/ECC · error · ValueError
File changed while hashing
Error message
File changed while hashing: {path} What it means
_fingerprint takes four stat snapshots (before open, after open, after read, after close) and requires the identity tuple (st_dev, st_ino, st_size, st_mtime_ns, st_ctime_ns) to be identical across all of them. If the file was modified, replaced, truncated, or re-created while being hashed, it raises this error so the digest is never trusted for a moving target.
Solutions
- Ensure all asset-producing steps complete before running ingest_assets (pipeline ordering/dependencies).
- Use a file lock or atomic rename (write to temp, then os.replace) in the producer.
- Re-run the ingest after the writer finishes; the error is transient by nature.
- Run only one ingestion job per asset directory at a time.
Example fix
# before subprocess.Popen(['download.sh']) ingest_assets(manifest) # races with download # after subprocess.run(['download.sh'], check=True) ingest_assets(manifest)
Defensive patterns
Strategy: retry
Validate before calling
# ensure no writer holds the file: lsof <path> returns empty import subprocess busy = subprocess.run(['lsof', path], capture_output=True).stdout
Try / catch
import time
for attempt in range(3):
try:
return ingest_assets(manifest)
except ValueError as e:
if 'File changed while hashing' not in str(e):
raise
time.sleep(2 ** attempt) Prevention
- Producers must write atomically (temp file + os.replace)
- Sequence pipeline steps: build finishes before ingest starts
- Avoid file-sync tools operating on the asset directory during runs
When it happens
Trigger: The asset file is written, truncated, renamed-over, or re-created by another process between the first stat and the final stat during a _fingerprint call.
Common situations: A build/download step still writing the asset when ingestion starts; two pipeline jobs ingesting and regenerating the same asset directory concurrently; editors or sync tools (Dropbox, rsync) touching files mid-run.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- capsule.busy
- capsule.lock_lost
- Legacy sync path changed before removal
- reference source mutated while creating stable snapshot
- Refusing to release a changed Claude settings lock
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/8e6b025e8e782843.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-application/scripts/tasteforge/assets.py:67
flags = os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0) | os.O_NONBLOCK
fd = os.open(path, flags)
with os.fdopen(fd, 'rb') as stream:
opened = os.fstat(stream.fileno())
if not stat.S_ISREG(opened.st_mode):
raise ValueError(f'Not a regular file: {path}')
digest = hashlib.sha256()
header = stream.read(12)
digest.update(header)
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
digest.update(chunk)
after = os.fstat(stream.fileno())
final = path.stat()
except OSError as exc:
raise ValueError(f'Cannot read local asset: {path}') from exc
def identity(info: os.stat_result) -> tuple[int, ...]:
return (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns)
if len({identity(info) for info in (before, opened, after, final)}) != 1:
raise ValueError(f'File changed while hashing: {path}')
if modality == '3d_asset':
if len(header) != 12:
raise ValueError(f'Truncated GLB header: {path}')
magic, version, size = struct.unpack('<4sII', header)
if magic != b'glTF' or version != 2 or size != final.st_size:
raise ValueError(f'Invalid GLB magic, version or declared size: {path}')
return dict(path=str(path), bytes=final.st_size, sha256=digest.hexdigest())
def _load(path: Path) -> dict[str, Any]:
_fingerprint(path)
try:
data = json.loads(path.read_text(encoding='utf-8'))
except (ValueError, OSError) as exc:
raise ValueError(f'Cannot read JSON object: {path}') from exc
if not isinstance(data, dict):
raise ValueError('Expected a JSON object')
return dataView on GitHub (pinned to 8321021c54)