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

  1. Identify what is modifying the path (close editors, pause sync/watchers) and re-run `mise dot recover`.
  2. Inspect the current content; if acceptable, use `mise dot recover <operation> --keep-current`.
  3. Run recovery while the system is quiet (no editors, sync agents, or concurrent mise commands) to win the race.
  4. 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

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


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)