jdx/mise · error

{} path(s) conflict (see the plan above); resolve them first

Error message

{} path(s) conflict (see the plan above); resolve them first{hint}

What it means

Before executing a restore, execute() computes a plan and detects conflicts: paths whose current on-disk state diverges from what the restore expects (including type changes like file→symlink). If any conflicts exist and the request is not forced, mise aborts with the conflict count, a pointer to the printed plan, and — when a conflicting path's type changed and --force was not given — a hint to pass --force.

Source

Thrown at src/system/history/replay.rs:420

        return Ok(());
    }
    let conflicts: Vec<&Step> = steps
        .iter()
        .filter(|step| matches!(step.action, Action::Conflict(_)))
        .collect();
    if !conflicts.is_empty() {
        // `--force` answers a type change only (and undo already forces);
        // an occupant history never captured stays a conflict whatever is
        // passed, so it is not offered where it would not help
        let type_changes = conflicts.iter().any(
            |step| matches!(&step.action, Action::Conflict(reason) if reason.contains(" became ")),
        );
        let hint = if !exec.force && type_changes {
            "; pass --force to replace a path whose type changed"
        } else {
            ""
        };
        bail!(
            "{} path(s) conflict (see the plan above); resolve them first{hint}",
            conflicts.len()
        );
    }
    // a link to a directory is not the directory: it counts as work left
    let restores = exec
        .restore_dirs
        .iter()
        .filter(|dir| std::fs::symlink_metadata(dir).is_err())
        .count();
    if !steps.iter().any(|step| step.action.mutates()) && restores == 0 {
        info!("history: nothing to do");
        return Ok(());
    }
    if !exec.yes && !prompt::confirm("history: apply this plan?")?.is_yes() {
        info!("history: skipped");
        return Ok(());
    }

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Review the printed plan, resolve the listed conflicting paths manually, then re-run
  2. Re-run with --force to replace paths whose type changed: `... --force`
  3. Commit or stash local edits so the restore can proceed cleanly, then rollback/undo

Example fix

// before
mise bootstrap dotfiles undo          # 3 path(s) conflict, type changed
// after
mise bootstrap dotfiles undo --force  # accept replacement of type-changed paths
Defensive patterns

Strategy: try-catch

Validate before calling

const plan = await previewPlan(req);
if (plan.conflicts.length > 0) {
  console.warn(plan.conflicts.join("\n"));
  if (!force) throw new Error("conflicts present; pass --force");
}

Type guard

const isClean = (plan: Plan): plan is Plan & { conflicts: [] } =>
  plan.conflicts.length === 0;

Try / catch

try {
  await undo();
} catch (e) {
  if (String(e).includes("path(s) conflict")) {
    if (typeChangedConfirmed) {
      await undo({ force: true });
    } else {
      console.error("Resolve conflicts shown in the plan first.");
    }
  } else throw e;
}

Prevention

When it happens

Trigger: execute (via rollback or undo) finds conflicts.len() > 0 and exec.force is false; the extra --force hint appears when type_changes is true. Typical causes: files edited locally after the checkpoint, symlink↔file swaps, different ownership/targets.

Common situations: User edited dotfiles since the checkpoint and now undoes; a tool replaced a symlink with a regular file (or vice versa); config managers (stow, chezmoi) changed link types.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/f3150262d0aad257. Report an issue: GitHub.