{"record":{"id":"3d9f764c08811f38","repo":"affaan-m/ECC","slug":"refusing-to-action-managed-destination-changed","errorCode":null,"errorMessage":"Refusing to ${action}: managed destination changed before removal; replacement preserved at ${quarantinePath}.","messagePattern":"Refusing to (.+?): managed destination changed before removal; replacement preserved at (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/install-lifecycle.js","lineNumber":559,"sourceCode":"    path.dirname(managedDestination.canonicalRoot),\n    '.ecc-remove-'\n  ));\n  const quarantinePath = path.join(quarantineDir, path.basename(finalDestination));\n\n  try {\n    fs.renameSync(finalDestination, quarantinePath);\n  } catch (error) {\n    fs.rmdirSync(quarantineDir);\n    throw error;\n  }\n\n  const quarantinedStat = fs.lstatSync(quarantinePath, { bigint: true });\n  if (!hasSameFileIdentity(expectedStat, quarantinedStat)) {\n    try {\n      fs.renameSync(quarantinePath, finalDestination);\n      fs.rmdirSync(quarantineDir);\n    } catch (_restoreError) {\n      throw new Error(\n        `Refusing to ${action}: managed destination changed before removal; replacement preserved at ${quarantinePath}.`\n      );\n    }\n    throw createChangedDestinationError(action);\n  }\n\n  if (quarantinedStat.isDirectory() && !options.recursive) {\n    fs.rmdirSync(quarantinePath);\n  } else {\n    fs.rmSync(quarantinePath, options);\n  }\n  fs.rmdirSync(quarantineDir);\n  return finalDestination;\n}\n\nfunction deepMergeJson(baseValue, patchValue) {\n  if (!isPlainObject(baseValue) || !isPlainObject(patchValue)) {\n    return cloneJsonValue(patchValue);","sourceCodeStart":541,"sourceCodeEnd":577,"githubUrl":"https://github.com/affaan-m/ECC/blob/06c5e118c4d3e6c3b7f9445f973a2194c82de193/scripts/lib/install-lifecycle.js#L541-L577","documentation":"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.","triggerScenarios":"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.","commonSituations":"Running uninstall/repair while an editor or agent session is actively rewriting .claude config files; parallel CI jobs sharing one home directory.","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"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    return runLifecycleOperation();\n  } catch (error) {\n    if (!/changed before removal/.test(error.message)) throw error;\n    // transient race: a concurrent writer replaced the destination.\n    // The quarantine path in error.message holds the replacement content.\n    await sleep(500 * (attempt + 1)); // let writers settle, then retry\n  }\n}\nthrow new Error('destination kept changing during removal; inspect the quarantine copy');","preventionTips":["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"],"tags":["filesystem","race-condition","toctou","uninstall","atomicity"],"backgroundTag":"toctou-race-detected","analyzedSha":"06c5e118c4d3e6c3b7f9445f973a2194c82de193","analyzedAt":"2026-08-18T11:27:13.915Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}