jdx/mise · error

target changed after unapply planning

Error message

target changed after unapply planning

What it means

During unapply, mise first snapshots each target file's text (the plan), then validate_unapply re-reads the file and compares it to the planned text. If the file's content changed between planning and validation — including switching to/from a symlink — mise aborts rather than unapply against stale assumptions.

Source

Thrown at src/system/edits.rs:968

}

/// Ensure template functions or another concurrent actor did not invalidate
/// any edit ownership checks performed during planning.
pub(crate) fn validate_unapply(todo: &[UnapplyPlan<'_>]) -> Result<()> {
    let mut checked = indexmap::IndexSet::new();
    let mut problems = vec![];
    for plan in todo {
        if !checked.insert(plan.req.path.clone()) {
            continue;
        }
        let result = if plan.req.path.is_symlink() {
            Err(eyre::eyre!("{SYMLINK_REASON}"))
        } else {
            file::read_to_string(&plan.req.path).and_then(|text| {
                if text == plan.text {
                    Ok(())
                } else {
                    bail!("target changed after unapply planning")
                }
            })
        };
        if let Err(err) = result {
            problems.push(format!(
                "  \"{}\" ({}): {err}",
                plan.req.path_raw,
                plan.req.describe_op()
            ));
        }
    }
    if !problems.is_empty() {
        bail!(
            "edits: cannot unapply these entries:\n{}",
            problems.join("\n")
        );
    }
    Ok(())

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Re-run the unapply command — a fresh plan will pick up the new file contents.
  2. Find what modified the file concurrently (editor autosave, sync daemon, watcher) and pause it.
  3. If the file flipped to a symlink, edit the real file manually instead.

Example fix

# before: file changed mid-run
$ mise edits unapply
error: target changed after unapply planning

# after: retry when nothing else touches the files
$ mise edits unapply  # fresh plan succeeds
Defensive patterns

Strategy: retry

Validate before calling

# snapshot and compare to detect concurrent writers
md5sum ~/.zshrc; mise edits unapply; md5sum ~/.zshrc

Try / catch

// retry the whole unapply once with a fresh plan
match run_unapply().await {
    Err(e) if e.to_string().contains("target changed after unapply planning") => retry_with_fresh_plan().await,
    other => other,
}

Prevention

When it happens

Trigger: Calling validate_unapply when file::read_to_string(plan.req.path) returns text different from plan.text, i.e. the file was modified after plan_unapply ran (or the path became a symlink, which is rejected separately with SYMLINK_REASON).

Common situations: An editor, formatter, file watcher, or another mise/command run rewrote the dotfile between planning and execution of an unapply; or a sync process (e.g. Dropbox, a dotfile manager) touched the file mid-run.

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 jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/e1fd6ce9b9a95d10. Report an issue: GitHub.