affaan-m/ECC · error
Refusing to read a file that changed during validation
Error message
Refusing to read a file that changed during validation: ${filePath} What it means
hashFileNoFollow computes a SHA-256 digest of a file while guarding against symlink and time-of-check-time-of-use (TOCTOU) attacks. Before hashing it stats the path twice and compares dev/ino/size/mtime/ctime; if the path is a symlink, not a regular file, or any attribute changed between the two stats, it refuses to read and throws this error. It exists so installers never hash a file that could be swapped mid-read.
Solutions
- Rerun the hashing/validation step — a transient concurrent write usually won't repeat.
- Close editors, pause file-sync tools, or stop concurrent installs that touch the path, then retry.
- If the path is a symlink, resolve it (fs.realpath) to a regular file before hashing.
- Ensure the file is a regular file (fs.statSync(p).isFile()) before passing it in.
Example fix
// before: hashing a possibly-swapped path directly
const { digest } = hashFileNoFollow(configPath);
// after: snapshot the file to a stable temp copy first
const stable = fs.realpathSync(configPath);
if (!fs.statSync(stable).isFile()) throw new Error('not a regular file');
const { digest } = hashFileNoFollow(stable); Defensive patterns
Strategy: retry
Validate before calling
const st = fs.lstatSync(p);
if (st.isSymbolicLink() || !st.isFile()) throw new Error(`cannot hash ${p}: symlink or not a regular file`); Type guard
function isHashableRegularFile(p) { const st = fs.lstatSync(p); return st.isFile() && !st.isSymbolicLink(); } Try / catch
try {
const { digest } = hashFileNoFollow(p);
} catch (e) {
if (e.message.startsWith('Refusing to read a file that changed during validation')) {
await new Promise(r => setTimeout(r, 100));
return hashFileNoFollow(p); // retry after transient writer settles
}
throw e;
} Prevention
- Pause file-sync clients and close editors holding the target open during hashing.
- Never run concurrent installers against the same directory.
- Resolve symlinks to real paths before hashing.
- Verify the path is a regular file with lstat before calling the API.
When it happens
Trigger: Calling hashFileNoFollow(filePath) when: (1) the file at filePath is replaced, truncated, or modified between the initial stat and the re-stat (race with another writer such as a running installer, editor, or sync tool); (2) filePath resolves to a symlink (finalPathStat.isSymbolicLink()); (3) filePath is a directory, FIFO, socket, or any non-regular file.
Common situations: A sync client (Dropbox, iCloud) rewrites the file during hashing; an editor auto-save fires mid-validation; a malicious or accidental symlink swap during a legacy migration; a concurrent ECC install running in another terminal touching the same managed file.
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
- gate.variant_invalid
- Invalid ECC repo root: unreadable package.json at
- Memory destination changed while it was being created.
- output artifact must be a regular file
- output bundle contains a symlink
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/b342332848afc380.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/install/opencode-legacy-migration.js:123
const before = fs.fstatSync(descriptor, { bigint: true });
if (!before.isFile()) {
throw new Error(`Refusing to read a non-file at ${filePath}`);
}
const content = fs.readFileSync(descriptor);
const after = fs.fstatSync(descriptor, { bigint: true });
const finalPathStat = fs.lstatSync(filePath, { bigint: true });
const unchanged = before.dev === after.dev
&& before.ino === after.ino
&& before.size === after.size
&& before.mtimeMs === after.mtimeMs
&& before.ctimeMs === after.ctimeMs
&& after.dev === finalPathStat.dev
&& after.ino === finalPathStat.ino
&& after.size === finalPathStat.size
&& after.mtimeMs === finalPathStat.mtimeMs
&& after.ctimeMs === finalPathStat.ctimeMs;
if (finalPathStat.isSymbolicLink() || !finalPathStat.isFile() || !unchanged) {
throw new Error(`Refusing to read a file that changed during validation: ${filePath}`);
}
return {
digest: crypto.createHash('sha256').update(content).digest('hex'),
stat: after,
};
} finally {
fs.closeSync(descriptor);
}
}
function removeEmptyParents(startPath, legacyRoot) {
let currentPath = path.dirname(startPath);
while (!samePath(currentPath, legacyRoot)) {
const safePath = assertWithinTrustedRoot(
currentPath,
legacyRoot,
'clean legacy OpenCode install'
);View on GitHub (pinned to 8321021c54)