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

  1. Inspect the file at the quarantine path printed in the message, merge/preserve any changes you need, then delete it manually.
  2. Close editors, sync clients, and other processes touching the file, then re-run the uninstall/removal operation.
  3. Re-run the lifecycle operation once nothing holds locks on the destination; on Windows check antivirus exclusion for the directory.
  4. 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

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


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)