{"record":{"id":"b342332848afc380","repo":"affaan-m/ECC","slug":"refusing-to-read-a-file-that-changed-during-validation","errorCode":null,"errorMessage":"Refusing to read a file that changed during validation: ${filePath}","messagePattern":"Refusing to read a file that changed during validation: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/lib/install/opencode-legacy-migration.js","lineNumber":123,"sourceCode":"    const before = fs.fstatSync(descriptor, { bigint: true });\n    if (!before.isFile()) {\n      throw new Error(`Refusing to read a non-file at ${filePath}`);\n    }\n    const content = fs.readFileSync(descriptor);\n    const after = fs.fstatSync(descriptor, { bigint: true });\n    const finalPathStat = fs.lstatSync(filePath, { bigint: true });\n    const unchanged = before.dev === after.dev\n      && before.ino === after.ino\n      && before.size === after.size\n      && before.mtimeMs === after.mtimeMs\n      && before.ctimeMs === after.ctimeMs\n      && after.dev === finalPathStat.dev\n      && after.ino === finalPathStat.ino\n      && after.size === finalPathStat.size\n      && after.mtimeMs === finalPathStat.mtimeMs\n      && after.ctimeMs === finalPathStat.ctimeMs;\n    if (finalPathStat.isSymbolicLink() || !finalPathStat.isFile() || !unchanged) {\n      throw new Error(`Refusing to read a file that changed during validation: ${filePath}`);\n    }\n    return {\n      digest: crypto.createHash('sha256').update(content).digest('hex'),\n      stat: after,\n    };\n  } finally {\n    fs.closeSync(descriptor);\n  }\n}\n\nfunction removeEmptyParents(startPath, legacyRoot) {\n  let currentPath = path.dirname(startPath);\n  while (!samePath(currentPath, legacyRoot)) {\n    const safePath = assertWithinTrustedRoot(\n      currentPath,\n      legacyRoot,\n      'clean legacy OpenCode install'\n    );","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/install/opencode-legacy-migration.js#L105-L141","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: hashing a possibly-swapped path directly\nconst { digest } = hashFileNoFollow(configPath);\n\n// after: snapshot the file to a stable temp copy first\nconst stable = fs.realpathSync(configPath);\nif (!fs.statSync(stable).isFile()) throw new Error('not a regular file');\nconst { digest } = hashFileNoFollow(stable);","handlingStrategy":"retry","validationCode":"const st = fs.lstatSync(p);\nif (st.isSymbolicLink() || !st.isFile()) throw new Error(`cannot hash ${p}: symlink or not a regular file`);","typeGuard":"function isHashableRegularFile(p) { const st = fs.lstatSync(p); return st.isFile() && !st.isSymbolicLink(); }","tryCatchPattern":"try {\n  const { digest } = hashFileNoFollow(p);\n} catch (e) {\n  if (e.message.startsWith('Refusing to read a file that changed during validation')) {\n    await new Promise(r => setTimeout(r, 100));\n    return hashFileNoFollow(p); // retry after transient writer settles\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["filesystem","race-condition","security","symlink"],"backgroundTag":"checksum-mismatch","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}