jdx/mise · warning
changed while preparing recovery; left untouched
Error message
changed while preparing recovery; left untouched
What it means
recover_path performs a final liveness re-check just before restoring: it validates the prior snapshot against the store and re-observes the path, requiring it to still equal the `after` snapshot. If the path changed in the window between the earlier check and the restore, mise aborts to avoid restoring over freshly changed data.
Solutions
- Identify what is modifying the path (close editors, pause sync/watchers) and re-run `mise dot recover`.
- Inspect the current content; if acceptable, use `mise dot recover <operation> --keep-current`.
- Run recovery while the system is quiet (no editors, sync agents, or concurrent mise commands) to win the race.
- Repeat the recovery — this is a TOCTOU-style race, so an uncontended retry typically succeeds.
Example fix
// before: recovering while editor autosaves $ vim ~/.zshrc & mise dot recover # changed while preparing recovery // after :quit # or pause autosave/sync $ mise dot recover
Defensive patterns
Strategy: retry
Validate before calling
export function observeQuietly(path: string, samples = 2, delayMs = 200): Promise<string[]> {
return Promise.all(Array.from({ length: samples }, async (_, i) => {
await sleep(i * delayMs);
return statHash(path); // stable across samples means path is quiet
}));
} Try / catch
try {
await dotRecover(op);
} catch (e) {
if (/changed while preparing recovery/.test(String(e))) {
await sleep(1000); // let the interfering writer finish
await dotRecover(op); // uncontended retry
} else throw e;
} Prevention
- Pause autosave/formatters and file sync during recovery
- Run only one mise instance at a time
- Retry recovery in a quiet session; the race usually resolves
When it happens
Trigger: recover_path passes all earlier checks, validate_snapshot succeeds, but `PathState::observe(path)` no longer equals `after` immediately before restore — something modified the path between the two observations (race with another process/editor).
Common situations: An editor or file watcher (autosave, formatter-on-save, sync daemon) touched the dotfile during recovery; another mise instance operated on the same path concurrently; an antivirus/backup tool touched metadata changing the observed state.
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
- changed after the operation; left untouched
- interrupted file recovery needs attention; temporary…
- brew-cask: refusing elevated operation because target…
- brew-cask: temporary artifact directory was replaced
- cannot safely change
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/05f838430c3f8a42.
Report an issue: GitHub.
Appendix: source
Thrown at src/system/history/recovery.rs:104
bail!("changed after the operation; left untouched");
}
// Entry count alone cannot establish a directory's identity. Never
// replace a populated directory on that evidence.
if matches!(after, PathState::Dir { entries, .. } if *entries != 0)
&& !(matches!(prior, PathSnapshot::Directory { .. })
&& matches!(
after,
PathState::Dir {
identity: Some(_),
..
}
))
{
bail!("directory contents cannot be verified safely; left untouched");
}
validate_snapshot(state_dir, prior)?;
if PathState::observe(path) != *after {
bail!("changed while preparing recovery; left untouched");
}
restore(state_dir, path, prior)
}
fn validate_destination(path: &Path) -> Result<()> {
if !path.is_absolute() || path.components().any(|c| matches!(c, Component::ParentDir)) {
bail!("invalid recovery destination");
}
for parent in path.ancestors().skip(1) {
if std::fs::symlink_metadata(parent).is_ok_and(|meta| meta.is_symlink()) {
bail!("a parent directory is now a symlink; left untouched");
}
}
Ok(())
}
fn validate_blob_id(hash: &str) -> Result<()> {
if hash.len() != 64 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {View on GitHub (pinned to 533346cc37)