{"record":{"id":"05f838430c3f8a42","repo":"jdx/mise","slug":"changed-while-preparing-recovery-left-untouched","errorCode":null,"errorMessage":"changed while preparing recovery; left untouched","messagePattern":"changed while preparing recovery; left untouched","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/system/history/recovery.rs","lineNumber":104,"sourceCode":"        bail!(\"changed after the operation; left untouched\");\n    }\n    // Entry count alone cannot establish a directory's identity. Never\n    // replace a populated directory on that evidence.\n    if matches!(after, PathState::Dir { entries, .. } if *entries != 0)\n        && !(matches!(prior, PathSnapshot::Directory { .. })\n            && matches!(\n                after,\n                PathState::Dir {\n                    identity: Some(_),\n                    ..\n                }\n            ))\n    {\n        bail!(\"directory contents cannot be verified safely; left untouched\");\n    }\n    validate_snapshot(state_dir, prior)?;\n    if PathState::observe(path) != *after {\n        bail!(\"changed while preparing recovery; left untouched\");\n    }\n    restore(state_dir, path, prior)\n}\n\nfn validate_destination(path: &Path) -> Result<()> {\n    if !path.is_absolute() || path.components().any(|c| matches!(c, Component::ParentDir)) {\n        bail!(\"invalid recovery destination\");\n    }\n    for parent in path.ancestors().skip(1) {\n        if std::fs::symlink_metadata(parent).is_ok_and(|meta| meta.is_symlink()) {\n            bail!(\"a parent directory is now a symlink; left untouched\");\n        }\n    }\n    Ok(())\n}\n\nfn validate_blob_id(hash: &str) -> Result<()> {\n    if hash.len() != 64 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/history/recovery.rs#L86-L122","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: recovering while editor autosaves\n$ vim ~/.zshrc &  mise dot recover   # changed while preparing recovery\n// after\n:quit  # or pause autosave/sync\n$ mise dot recover","handlingStrategy":"retry","validationCode":"export function observeQuietly(path: string, samples = 2, delayMs = 200): Promise<string[]> {\n  return Promise.all(Array.from({ length: samples }, async (_, i) => {\n    await sleep(i * delayMs);\n    return statHash(path); // stable across samples means path is quiet\n  }));\n}","typeGuard":null,"tryCatchPattern":"try {\n  await dotRecover(op);\n} catch (e) {\n  if (/changed while preparing recovery/.test(String(e))) {\n    await sleep(1000);          // let the interfering writer finish\n    await dotRecover(op);       // uncontended retry\n  } else throw e;\n}","preventionTips":["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"],"tags":["recovery","race-condition","dotfiles","filesystem"],"backgroundTag":"invalid-state-transition","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}