jdx/mise · error
target changed after unapply planning
Error message
target changed after unapply planning
What it means
Thrown by `validate_unapply` after the unapply plan was built: it re-reads each planned target file and compares its current text with the text captured at planning time (`plan.text`). If they differ, the file changed between planning and validation, so applying the plan could corrupt unintended edits; the error aborts the whole unapply and is aggregated into the problems list.
Solutions
- Re-run the unapply command; the newly built plan will match the current file state.
- Stop or pause concurrent processes (formatters, watchers, sync tools) that write to the target files while unapplying.
- Ensure the path is a regular file, not a symlink, since symlinked targets are not supported for unapply.
- Verify no other instance of the tool is operating on the same files concurrently.
Example fix
// before: running unapply while a watcher rewrites config $ tool edits unapply & $ formatter --watch # rewrites file mid-plan // after: stop the watcher first $ formatter stop && tool edits unapply
Defensive patterns
Strategy: retry
Validate before calling
let before = fs::read_to_string(&path)?;
// ... plan ...
if fs::read_to_string(&path)? != before {
eprintln!("{} changed during planning; aborting", path.display());
} Try / catch
for attempt in 0..3 {
match tool.unapply() {
Err(e) if e.to_string().contains("target changed after unapply planning") => continue,
other => { other?; break; }
}
} Prevention
- Stop file watchers, formatters, and sync tools during unapply.
- Run unapply serially; never run two instances concurrently.
- Do not edit target files while the command is running.
When it happens
Trigger: Calling the unapply flow where the target file's contents change between plan_unapply (which snapshots file text) and validate_unapply (which re-reads it) — e.g. another process writes to the file in that window, or the path is a symlink swapped in between the two steps (symlinked paths bail earlier with SYMLINK_REASON).
Common situations: A file watcher, formatter, or editor auto-saved the file while the unapply command was running; a build tool regenerated config files concurrently; two instances of the tool ran at once on the same machine.
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
- brew-cask: : Homebrew took ownership of this cask while…
- changed while the changes were being applied; nothing more…
- file changed during lockfile generation; retry the command
- lockfile changed while generation was being prepared; retry…
- these paths keep changing while being protected; nothing…
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/e1fd6ce9b9a95d10.
Report an issue: GitHub.
Appendix: 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 533346cc37)