jdx/mise · error

changed during recovery; left untouched

Error message

{} changed during recovery; left untouched

What it means

During pull application, recover_step restores each path to its pre-image (`step.before`) when a step must be rolled back or reconciled. Before touching the file it re-reads the live object and permissions: if the live content no longer equals the planned pending object/mode, the world no longer matches what the recovery step was computed for, so mise refuses to touch the file and reports it was 'left untouched'. This prevents recovery from clobbering concurrent user edits.

Solutions

  1. Inspect the reported path — mise deliberately left it untouched; decide manually whether to keep your edit or let the next `mise dot sync` overwrite it.
  2. Re-run `mise dot sync` after resolving the file; a fresh plan will be computed from current state.
  3. If you want the pulled version, revert your local change (or stash it) and re-run the sync.
  4. Avoid editing dotfiles while a sync is in progress.
Defensive patterns

Strategy: validation

Validate before calling

// confirm the file is in the expected state before re-running sync
current=$(sha256sum ~/.config/target | cut -d' ' -f1)
echo "live hash: $current — if you edited it, resolve manually before syncing"

Prevention

When it happens

Trigger: Recovery (rollback or crash-recovery replay of a Step) runs after the file's live content or permissions changed from what the plan captured: `current != step.pending.object || live_permissions != step.desired_mode`. Typically another process modified the dotfile between planning and recovery.

Common situations: A user edits a dotfile while a pull is mid-flight and the pull must roll back; a crash leaves a stale journal and by the time recovery replays, the file was edited or replaced; two syncs race on the same file.

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/d6b180ba270ea19d. Report an issue: GitHub.

Appendix: source

Thrown at src/system/history/sync/apply.rs:796

    repo: &crate::system::history::shadow::HistoryRepo,
    head: &str,
) -> Result<()> {
    if crate::config::Settings::get()
        .history
        .allow_plaintext_history
    {
        return Ok(());
    }
    super::files::audit_history(repo, head, &Default::default())
}

fn recover_step(repo: &crate::system::history::shadow::HistoryRepo, step: &Step) -> Result<()> {
    let current = live_object(repo, &step.path)?;
    if current == step.before && live_permissions(&step.path)? == step.before_mode {
        return Ok(());
    }
    if current != step.pending.object || live_permissions(&step.path)? != step.desired_mode {
        bail!(
            "{} changed during recovery; left untouched",
            display_path(&step.path)
        );
    }
    match &step.before {
        Some((mode, oid)) => {
            #[cfg(unix)]
            let bits = {
                use std::os::unix::fs::PermissionsExt;
                step.permissions.as_ref().map(|p| p.mode() & 0o777)
            };
            #[cfg(not(unix))]
            let bits = None;
            replay::write_path_with_mode(repo, &step.path, mode, oid, bits)?;
        }
        None => replay::remove(&step.path)?,
    }
    if !step.path.is_symlink()

View on GitHub (pinned to 533346cc37)