affaan-m/ECC · error

Refusing to hash changed install destination

Error message

Refusing to hash changed install destination: ${operation.destinationPath}

What it means

readInstalledFileNoFollow opens the install destination without following symlinks and then re-stats the final path to verify the opened file is still the same regular file (same inode/dev). If the opened descriptor and the path-resolved stat disagree — the destination was swapped, replaced, or is no longer a regular file between open and stat — hashing would attribute a digest to the wrong bytes, so it refuses. This is a TOCTOU (time-of-check-time-of-use) integrity guard for install destinations.

Solutions

  1. Re-run the install — transient races usually pass on retry once nothing else touches the directory.
  2. Ensure no other process modifies the install target while the installer runs (close sync clients/watchers).
  3. Run only one installer instance at a time against the same targetRoot.
  4. If it persists, delete the destination and re-apply the plan so a fresh file is installed.
Defensive patterns

Strategy: retry

Try / catch

try {
  applyInstallPlan(plan);
} catch (e) {
  if (e.message.startsWith('Refusing to hash changed install destination')) {
    // transient race: pause and retry once with no concurrent writers
    await new Promise(r => setTimeout(r, 250));
    return applyInstallPlan(plan);
  }
  throw e;
}

Prevention

When it happens

Trigger: Another process (editor, sync tool, concurrent installer run) replaces or renames the destination file after openFileSync but before fstat/lstat verification; destination swapped with a symlink or non-file; race between two concurrent applyInstallPlan runs on the same target.

Common situations: Running the installer while a cloud-sync client (Dropbox/OneDrive) rewrites the target directory; two installer processes running in parallel; antivirus or a build watcher touching the destination mid-install.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/3a0ddeaefb0587f7. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/install/apply.js:124

  try {
    descriptor = fs.openSync(operation.destinationPath, flags);
  } catch (error) {
    if (error.code === 'ENOENT') {
      return null;
    }
    throw error;
  }

  try {
    const openedStat = fs.fstatSync(descriptor, { bigint: true });
    const finalPathStat = fs.lstatSync(operation.destinationPath, { bigint: true });
    if (finalPathStat.isSymbolicLink() || !finalPathStat.isFile()) {
      return null;
    }
    const identityMatches = openedStat.ino === finalPathStat.ino
      && (!openedStat.dev || !finalPathStat.dev || openedStat.dev === finalPathStat.dev);
    if (!openedStat.isFile() || !identityMatches) {
      throw new Error(
        `Refusing to hash changed install destination: ${operation.destinationPath}`
      );
    }
    // Revalidate the full path after opening. The descriptor pins the file so
    // the digest and metadata refer to the same object.
    assertSafeInstallOperation(plan, operation);
    assertSafeClaudeSkillOperation(plan, operation);
    return fs.readFileSync(descriptor);
  } finally {
    fs.closeSync(descriptor);
  }
}

function stateWithContentDigests(state, plan) {
  const currentDestinations = new Set((plan.operations || [])
    .filter(operation => operation.destinationPath)
    .map(operation => {
      const resolved = path.resolve(operation.destinationPath);

View on GitHub (pinned to 8321021c54)