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

  1. Close editors, file sync clients, and other processes that touch the path, then rerun `mise bootstrap dotfiles recover`.
  2. If the current contents are what you want, accept them explicitly with `recover <operation> --keep-current`.
  3. 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

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


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