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
- Run the installer's uninstall/cleanup for the old target so the stale install-state is removed, then reinstall with the new plan.
- Delete or move the stale install-state file at plan.installStatePath if you are sure its recorded files are no longer needed.
- Re-run the install with the SAME target/root as the previous one so the plan matches recorded state.
- 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
- Always uninstall before changing --target or moving the install root.
- Keep one install-state file per target and don't reuse directories across targets.
- Check install-state content after version upgrades that may change adapter ids.
- Back up user-owned files before clearing stale state.
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
- Announcements discussion category is required
- Another ECC process is updating Claude settings
- At least one guided harness must be selected
- ${blockingIssues.map(issue => issue.message).join('; ')}
- Cannot adapt Antigravity agent
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)