affaan-m/ECC · error

Refusing install: install-state target does not match the…

Error message

Refusing install: install-state target does not match the current plan at ${plan.installStatePath}.

What it means

prepareUserOwnedFileGuard reads the previous install-state file and verifies it describes the same target as the current install plan (adapter id, target root, and install-state path). If a previous state exists whose target differs from the current plan, the installer refuses rather than overwrite user-owned files recorded for a different destination. This protects against clobbering a different harness/project layout recorded earlier.

Solutions

  1. Run the installer's uninstall/cleanup for the old target so the stale install-state is removed, then reinstall with the new plan.
  2. Delete or move the stale install-state file at plan.installStatePath if you are sure its recorded files are no longer needed.
  3. Re-run the install with the SAME target/root as the previous one so the plan matches recorded state.
  4. If migrating intentionally, back up user-owned files first, clear state, then install to the new location.

Example fix

// before: switching target while old state exists
runInstall({ target: 'claude-project', targetRoot: './new-dir' }); // throws

// after: clear old state (or uninstall) first
fs.rmSync(path.join('./new-dir', '.ecc-install-state.json'), { force: true });
runInstall({ target: 'claude-project', targetRoot: './new-dir' });
Defensive patterns

Strategy: validation

Validate before calling

const prev = fs.existsSync(statePath) ? JSON.parse(fs.readFileSync(statePath, 'utf8')) : null;
if (prev && (prev.target?.id !== plan.adapter.id || prev.target?.root !== plan.targetRoot)) {
  // clear or migrate stale state before installing
}

Type guard

function stateMatchesPlan(prev, plan) {
  return !prev || (prev.target?.id === plan.adapter.id
    && normPath(prev.target?.root) === normPath(plan.targetRoot)
    && normPath(prev.target?.installStatePath) === normPath(plan.installStatePath));
}

Try / catch

try {
  await migration(plan);
} catch (e) {
  if (e.message.includes('install-state target does not match')) {
    throw new Error(`Stale install state at ${plan.installStatePath}; run uninstall or remove the state file before switching targets.`, { cause: e });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling migration()/prepareUserOwnedFileGuard when the existing install-state at plan.installStatePath records previousState.target.id !== plan.adapter.id, a different targetRoot, or a different installStatePath — i.e., installing with a changed --target, a moved install root, or a relocated state file while old state persists.

Common situations: Switching from a global install to a project install (or vice versa) with stale state; renaming/moving the project directory after an install; upgrading across a version that changed the adapter id; pointing --target at a different harness while old install-state remains.

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

Appendix: source

Thrown at scripts/lib/install/ownership-guard.js:48

 * which makes a later uninstall delete the user's file.
 *
 * This guard generalises the Claude flat-skill migration conflict pattern to
 * every adapter copy operation: when a destination exists and is NOT recorded
 * as an ECC-managed operation in the previous install-state, the operation is
 * skipped with a warning instead of overwriting and claiming ownership.
 *
 * All managed targets share this ownership boundary (#2964).
 */
function prepareUserOwnedFileGuard(plan, migration) {
  const previousState = pathExists(plan.installStatePath)
    ? readInstallState(plan.installStatePath)
    : null;
  if (previousState && (
    previousState.target.id !== plan.adapter.id
    || comparablePath(previousState.target.root) !== comparablePath(plan.targetRoot)
    || comparablePath(previousState.target.installStatePath) !== comparablePath(plan.installStatePath)
  )) {
    throw new Error(`Refusing install: install-state target does not match the current plan at ${plan.installStatePath}.`);
  }
  // Recorded files remain updateable by reinstall/repair. Preserve their prior
  // digests if an attempt fails before writing them so uninstall detects drift.
  const previousManagedOperations = new Map(
    ((previousState && previousState.operations) || [])
      .filter(operation => (
        operation
        && operation.ownership === 'managed'
        && operation.destinationPath
      ))
      .map(operation => [comparablePath(operation.destinationPath), operation])
  );
  const managedDestinations = new Set(previousManagedOperations.keys());

  const appliedOperations = [];
  const skippedOperations = [];
  const warnings = [];
  for (const operation of (migration && migration.appliedOperations) || []) {

View on GitHub (pinned to 8321021c54)