affaan-m/ECC · error

Refusing install: a user-owned file appeared at

Error message

Refusing install: a user-owned file appeared at ${operation.destinationPath} after planning. Rerun the installer to preserve it.

What it means

assertNoNewUserOwnedFile is a last-line defense run while applying the install plan (under lock). For copy-file operations whose destination was NOT already a managed destination, it checks whether a file now exists at the destination that did not exist at planning time. If so, it assumes the user placed a file there and refuses the copy so the user's file is preserved.

Solutions

  1. Rerun the installer as the message says — the new planning pass will detect the existing file and treat it as user-owned (skip/preserve).
  2. If the file is not yours and should be overwritten, delete it or mark it as managed, then reinstall.
  3. Ensure no other process writes to the install directory while the installer runs.
  4. Verify the destination isn't being recreated by a git operation or postinstall script between plan and apply.

Example fix

// before: file appeared after planning → install aborts
applyInstallPlan(plan);

// after: re-plan so the existing user file is detected and preserved
const freshPlan = buildInstallPlan(options); // planning now sees the user file
applyInstallPlan(freshPlan);
Defensive patterns

Strategy: validation

Validate before calling

if (fs.existsSync(dest) && !plan.managedDestinations.has(norm(dest))) {
  console.warn(`user file exists at ${dest}; rerun installer to re-plan before applying`);
}

Type guard

function destinationIsSafeToCopy(op, managedDestinations) {
  return op.kind !== 'copy-file'
    || managedDestinations.has(norm(op.destinationPath))
    || !fs.existsSync(op.destinationPath);
}

Try / catch

try {
  applyInstallPlanLocked(plan);
} catch (e) {
  if (e.message.includes('user-owned file appeared')) {
    const fresh = buildInstallPlan(plan.options); // re-plan sees the new user file
    return applyInstallPlanLocked(fresh);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling applyInstallPlanLocked → assertNoNewUserOwnedFile when: (1) pathExists(operation.destinationPath) is true at apply time but the destination was absent (and unmanaged) during planning; (2) something created a file at that path between plan and apply — another process, a git checkout, or a hook.

Common situations: User creates a config file while the installer is running; a package manager or scaffolding tool writes the destination concurrently; rerunning an install after a partial failure where a previously-unmanaged file appeared; copying a repo that now contains the destination file.

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


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

Appendix: source

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

      ...skippedOperations,
    ],
    warnings: [...((migration && migration.warnings) || []), ...warnings],
    bridgeState,
    finalState,
    // Only keep bridge persistence when operations actually remain; a fully
    // skipped plan installs nothing and must not claim anything.
    requiresBridgeState: Boolean(migration.requiresBridgeState)
      && appliedOperations.length > 0,
  };
}

function assertNoNewUserOwnedFile(migration, operation) {
  if (operation.kind !== 'copy-file'
    || migration.managedDestinations.has(comparablePath(operation.destinationPath))
    || !pathExists(operation.destinationPath)) {
    return;
  }
  throw new Error(`Refusing install: a user-owned file appeared at ${operation.destinationPath} after planning. Rerun the installer to preserve it.`);
}

function preserveUnwrittenFiles(state, migration, writtenDestinations) {
  const writtenPaths = new Set([...writtenDestinations].map(comparablePath));
  return {
    ...state,
    operations: state.operations.filter(operation => (
      operation.kind !== 'copy-file'
      || migration.managedDestinations.has(comparablePath(operation.destinationPath))
      || writtenPaths.has(comparablePath(operation.destinationPath))
      || !pathExists(operation.destinationPath)
    )).map(operation => {
      const destination = comparablePath(operation.destinationPath);
      return operation.kind === 'copy-file' && !writtenPaths.has(destination)
        ? migration.previousManagedOperations.get(destination) || operation
        : operation;
    }),
  };

View on GitHub (pinned to 8321021c54)