affaan-m/ECC · error
Refusing to : managed destination changed before removal…
Error message
Refusing to ${action}: managed destination changed before removal; replacement preserved at ${quarantinePath}. What it means
During removal, the file is first renamed to a quarantine path; its identity (per lstat with bigint) is compared against the expected stat captured earlier. If the quarantined file no longer matches the expected identity — meaning the managed destination changed underneath the install lifecycle — the library tries to restore it. If the restore (rename back / rmdir) also fails, it throws this error naming the quarantine path where the replacement file was preserved, refusing to delete data it cannot safely account for.
Solutions
- Inspect the file at the quarantine path printed in the message, merge/preserve any changes you need, then delete it manually.
- Close editors, sync clients, and other processes touching the file, then re-run the uninstall/removal operation.
- Re-run the lifecycle operation once nothing holds locks on the destination; on Windows check antivirus exclusion for the directory.
- If changes were intentional, don't remove the file — update install-state (or re-run install) so the recorded state matches the on-disk file.
Example fix
// after error: replacement preserved at /tmp/ecc-quarantine-xyz/settings.json cp /tmp/ecc-quarantine-xyz/settings.json ~/.claude/settings.json.manual-backup rm -rf /tmp/ecc-quarantine-xyz # close editors/sync, then retry the operation
Defensive patterns
Strategy: try-catch
Validate before calling
const fs = require('fs');
function assertQuiescentFile(p) {
const s1 = fs.statSync(p, { bigint: true });
return () => {
const s2 = fs.statSync(p, { bigint: true });
if (s1.ino !== s2.ino || s1.size !== s2.size || s1.mtimeNs !== s2.mtimeNs) {
throw new Error('File changed during operation; close other processes and retry');
}
};
} Try / catch
try {
await uninstallManagedHooks(op);
} catch (err) {
if (/changed before removal; replacement preserved/.test(err.message)) {
const q = /preserved at (\S+)\.$/.exec(err.message)[1];
console.error(`Merge or delete the preserved file at ${q}, then re-run.`);
} else throw err;
} Prevention
- Close editors, file watchers, and sync clients that touch managed files before running uninstall/repair.
- Do not run install and uninstall concurrently on the same machine.
- After hitting this error, always inspect the preserved quarantine file before deleting anything.
When it happens
Trigger: Another process (editor save, sync tool, concurrent install/uninstall) modifies or replaces the managed file between the identity snapshot and the removal, AND the rename-back to finalDestination or rmdir of the quarantine dir fails (e.g. destination recreated by another process, permissions issue, AV lock on Windows).
Common situations: A file watcher/editor auto-reformats settings.json during uninstall; Dropbox/OneDrive sync locking or rewriting the file; two installs or an install plus uninstall running concurrently on the same machine; antivirus briefly holding the file open.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Issue # is not open
- a top-level committed transaction is required
- Another ECC process is updating Claude settings
- Another Nasiko lifecycle operation is already in progress…
- Another Nasiko lifecycle operation won lock acquisition
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/3d9f764c08811f38.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/install-lifecycle.js:631
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 8321021c54)