affaan-m/ECC · error · Error
Refusing to ${action}: managed destination changed before re
Error message
Refusing to ${action}: managed destination changed before removal; replacement preserved at ${quarantinePath}. What it means
TOCTOU defense during managed-path removal. The destination is first renamed into a quarantine directory, then its identity (device/inode via bigint stat) is compared with the pre-planned stat; a mismatch means the file changed between planning and removal. The code attempts to rename it back; if that restore also fails, the newer (replacement) content is left at the quarantine path and this error is thrown so nothing is silently destroyed.
Source
Thrown at scripts/lib/install-lifecycle.js:559
path.dirname(managedDestination.canonicalRoot),
'.ecc-remove-'
));
const quarantinePath = path.join(quarantineDir, path.basename(finalDestination));
try {
fs.renameSync(finalDestination, quarantinePath);
} catch (error) {
fs.rmdirSync(quarantineDir);
throw error;
}
const quarantinedStat = fs.lstatSync(quarantinePath, { bigint: true });
if (!hasSameFileIdentity(expectedStat, quarantinedStat)) {
try {
fs.renameSync(quarantinePath, finalDestination);
fs.rmdirSync(quarantineDir);
} catch (_restoreError) {
throw new Error(
`Refusing to ${action}: managed destination changed before removal; replacement preserved at ${quarantinePath}.`
);
}
throw createChangedDestinationError(action);
}
if (quarantinedStat.isDirectory() && !options.recursive) {
fs.rmdirSync(quarantinePath);
} else {
fs.rmSync(quarantinePath, options);
}
fs.rmdirSync(quarantineDir);
return finalDestination;
}
function deepMergeJson(baseValue, patchValue) {
if (!isPlainObject(baseValue) || !isPlainObject(patchValue)) {
return cloneJsonValue(patchValue);View on GitHub (pinned to 06c5e118c4)
Solutions
- Close editors/watchers and re-run the operation once the filesystem is quiet
- Inspect the quarantine path named in the message - it holds the replacement content that was preserved
- Avoid running two ECC install/repair/uninstall commands concurrently
Defensive patterns
Strategy: retry
Try / catch
for (let attempt = 0; attempt < 3; attempt++) {
try {
return runLifecycleOperation();
} catch (error) {
if (!/changed before removal/.test(error.message)) throw error;
// transient race: a concurrent writer replaced the destination.
// The quarantine path in error.message holds the replacement content.
await sleep(500 * (attempt + 1)); // let writers settle, then retry
}
}
throw new Error('destination kept changing during removal; inspect the quarantine copy'); Prevention
- Run only one install/repair/uninstall at a time per home directory
- Close editors and agent sessions that rewrite managed config files before lifecycle operations
- If the error persists, diff the quarantine copy named in the message to see what replaced the file
When it happens
Trigger: A concurrent process rewrites the destination between the installer's stat and its rename - editors doing atomic save-and-replace, two installer runs racing, sync tools touching configs mid-uninstall.
Common situations: Running uninstall/repair while an editor or agent session is actively rewriting .claude config files; parallel CI jobs sharing one home directory.
Related errors
- Legacy sync path changed while opening: ${filePath}
- Refusing to create non-regular legacy sync path: ${filePath}
- Legacy sync path changed before removal; preserved replaceme
- Legacy sync path changed before removal: ${filePath}
- ${label} must remain a regular, non-symlink file while it is
AI-assisted analysis of affaan-m/ECC@06c5e118c4 (2026-08-18).
Data as JSON: /api/errors/3d9f764c08811f38.
Report an issue: GitHub.