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

  1. Close editors/watchers and re-run the operation once the filesystem is quiet
  2. Inspect the quarantine path named in the message - it holds the replacement content that was preserved
  3. 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

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


AI-assisted analysis of affaan-m/ECC@06c5e118c4 (2026-08-18). Data as JSON: /api/errors/3d9f764c08811f38. Report an issue: GitHub.