affaan-m/ECC · error

Refusing to release a changed Claude settings lock

Error message

Refusing to release a changed Claude settings lock: ${lockPath}

What it means

When releasing the Claude settings lock, createSettingsLock quarantines the lock file by renaming it, then verifies the quarantined file's identity (inode/device via bigint lstat) matches the file this process originally locked. If the identity changed — someone replaced or recreated the lock between acquire and release — the library restores the quarantine if possible and refuses to delete a lock it may not own, preventing clobbering another process's lock.

Solutions

  1. Inspect lockPath manually: if no ECC process is active, remove the lock file and retry.
  2. Check whether another process or cron job is deleting/recreating lock files and stop it.
  3. Keep ECC processes short-lived relative to lock sweeper intervals to avoid lock replacement mid-run.
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await withSettingsLock(settingsPath, work);
} catch (e) {
  if (e.message.startsWith('Refusing to release a changed Claude settings lock')) {
    // lock was replaced; verify no ECC process running, then remove lockPath manually
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the release function created by createSettingsLock when the lock file at lockPath was replaced after acquisition (different inode/device), detected by sameFileIdentity(quarantinedStats, ownedStats) returning false.

Common situations: Another process force-deleted and recreated the stale lock; a cleanup cron removed the lock mid-run; the lock lives on a filesystem where rename/lstat identity behaves unexpectedly; long-running process outlived a lock sweeper.

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/84af4948e4b553ef. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/install/claude-settings-lock.js:53

    fs.closeSync(descriptor);
    descriptor = undefined;
    fs.linkSync(tempPath, lockPath);
  } catch (error) {
    if (descriptor !== undefined) fs.closeSync(descriptor);
    fs.rmSync(tempPath, { force: true });
    throw error;
  }
  fs.rmSync(tempPath, { force: true });

  let released = false;
  return () => {
    if (released) return;
    const quarantinePath = `${lockPath}.release-${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
    fs.renameSync(lockPath, quarantinePath);
    const quarantinedStats = fs.lstatSync(quarantinePath, { bigint: true });
    if (!sameFileIdentity(quarantinedStats, ownedStats)) {
      if (!fs.existsSync(lockPath)) fs.renameSync(quarantinePath, lockPath);
      throw new Error(`Refusing to release a changed Claude settings lock: ${lockPath}`);
    }
    released = true;
    fs.rmSync(quarantinePath, { force: true });
  };
}

function inspectSettingsLock(lockPath) {
  const descriptor = fs.openSync(lockPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
  try {
    const stats = fs.fstatSync(descriptor, { bigint: true });
    const pathStats = fs.lstatSync(lockPath, { bigint: true });
    if (
      !stats.isFile()
      || pathStats.isSymbolicLink()
      || !pathStats.isFile()
      || !sameFileIdentity(stats, pathStats)
    ) {
      return { metadata: null, stats };

View on GitHub (pinned to 8321021c54)