affaan-m/ECC · error
Refusing to hash changed install destination
Error message
Refusing to hash changed install destination: ${operation.destinationPath} What it means
readInstalledFileNoFollow opens the install destination without following symlinks and then re-stats the final path to verify the opened file is still the same regular file (same inode/dev). If the opened descriptor and the path-resolved stat disagree — the destination was swapped, replaced, or is no longer a regular file between open and stat — hashing would attribute a digest to the wrong bytes, so it refuses. This is a TOCTOU (time-of-check-time-of-use) integrity guard for install destinations.
Solutions
- Re-run the install — transient races usually pass on retry once nothing else touches the directory.
- Ensure no other process modifies the install target while the installer runs (close sync clients/watchers).
- Run only one installer instance at a time against the same targetRoot.
- If it persists, delete the destination and re-apply the plan so a fresh file is installed.
Defensive patterns
Strategy: retry
Try / catch
try {
applyInstallPlan(plan);
} catch (e) {
if (e.message.startsWith('Refusing to hash changed install destination')) {
// transient race: pause and retry once with no concurrent writers
await new Promise(r => setTimeout(r, 250));
return applyInstallPlan(plan);
}
throw e;
} Prevention
- Run only one installer process per target root at a time.
- Pause file-sync clients, watchers, and antivirus scans on the install directory during installs.
- Retry the install after a transient failure; if it repeats, investigate what is mutating the destination.
When it happens
Trigger: Another process (editor, sync tool, concurrent installer run) replaces or renames the destination file after openFileSync but before fstat/lstat verification; destination swapped with a symlink or non-file; race between two concurrent applyInstallPlan runs on the same target.
Common situations: Running the installer while a cloud-sync client (Dropbox/OneDrive) rewrites the target directory; two installer processes running in parallel; antivirus or a build watcher touching the destination mid-install.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- artifact byte count exceeded during reading
- artifact byte count mismatch
- artifact changed before reading
- artifact changed during reading
- Bundle changed during validation
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/3a0ddeaefb0587f7.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/install/apply.js:124
try {
descriptor = fs.openSync(operation.destinationPath, flags);
} catch (error) {
if (error.code === 'ENOENT') {
return null;
}
throw error;
}
try {
const openedStat = fs.fstatSync(descriptor, { bigint: true });
const finalPathStat = fs.lstatSync(operation.destinationPath, { bigint: true });
if (finalPathStat.isSymbolicLink() || !finalPathStat.isFile()) {
return null;
}
const identityMatches = openedStat.ino === finalPathStat.ino
&& (!openedStat.dev || !finalPathStat.dev || openedStat.dev === finalPathStat.dev);
if (!openedStat.isFile() || !identityMatches) {
throw new Error(
`Refusing to hash changed install destination: ${operation.destinationPath}`
);
}
// Revalidate the full path after opening. The descriptor pins the file so
// the digest and metadata refer to the same object.
assertSafeInstallOperation(plan, operation);
assertSafeClaudeSkillOperation(plan, operation);
return fs.readFileSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function stateWithContentDigests(state, plan) {
const currentDestinations = new Set((plan.operations || [])
.filter(operation => operation.destinationPath)
.map(operation => {
const resolved = path.resolve(operation.destinationPath);View on GitHub (pinned to 8321021c54)