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

  1. Ensure all asset-producing steps complete before running ingest_assets (pipeline ordering/dependencies).
  2. Use a file lock or atomic rename (write to temp, then os.replace) in the producer.
  3. Re-run the ingest after the writer finishes; the error is transient by nature.
  4. 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

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


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 data

View on GitHub (pinned to 8321021c54)