jdx/mise · error
changed while preparing recovery; left untouched
Error message
changed while preparing recovery; left untouched
What it means
During write-ahead recovery, recover_path re-checks that the target path's live state still matches the recorded 'after' state right before restoring the preimage. This bail fires when the path changed between the first observation and the second check (after validate_snapshot), i.e. something modified the file concurrently while recovery data was being validated. Recovery deliberately refuses to overwrite and leaves the path untouched so a concurrent editor's work is never clobbered.
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 afd2eddd3a)
Solutions
- Close editors, file sync clients, and other processes that touch the path, then rerun `mise bootstrap dotfiles recover`.
- If the current contents are what you want, accept them explicitly with `recover <operation> --keep-current`.
- Inspect the path, decide manually whether to keep it or restore the preimage, and retry recovery once the path is quiescent.
Example fix
// before (concurrent editor holding the file open) $ mise bootstrap dotfiles recover error: config: changed while preparing recovery; left untouched // after (editor closed, path stable) $ mise bootstrap dotfiles recover recovery complete
Defensive patterns
Strategy: retry
Validate before calling
None available — the race is internal to recover_path; ensure no other process is writing the target path before invoking recovery.
Try / catch
// match on the message to distinguish concurrency refusal from real corruption
match recover(&state_dir, &journal) {
Err(e) if e.to_string().contains("changed while preparing recovery") => {
// stop editors/sync clients, then retry recovery
}
Err(e) => return Err(e),
Ok(()) => {}
} Prevention
- Run recovery while editors, file-sync clients, and watchers touching the target paths are stopped
- Run only one mise instance against the same state directory at a time
- Use --keep-current when you know concurrent edits are intentional
When it happens
Trigger: Running `mise bootstrap dotfiles recover` (recover_entries -> recover_path) when another process or user modifies the target path between the initial `PathState::observe(path) != *after` check (line 85) and the re-check at line 103, which happens after validate_snapshot reads and hash-verifies all blobs.
Common situations: An editor, sync client (Dropbox/iCloud), backup agent, or another running mise instance touches the file while recovery is running; recovery of a large directory snapshot takes long enough that a watcher rewrites something in it.
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 during recovery; left untouched
- path component {} was concurrently created by another user
- {} changed while preparing enrollment; concurrent declaratio
- changed after the operation; left untouched
- a parent directory is now a symlink; left untouched
AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09).
Data as JSON: /api/errors/05f838430c3f8a42.
Report an issue: GitHub.