{"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":"error","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/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/history/recovery.rs#L86-L122","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (concurrent editor holding the file open)\n$ mise bootstrap dotfiles recover\nerror: config: changed while preparing recovery; left untouched\n\n// after (editor closed, path stable)\n$ mise bootstrap dotfiles recover\nrecovery complete","handlingStrategy":"retry","validationCode":"None available — the race is internal to recover_path; ensure no other process is writing the target path before invoking recovery.","typeGuard":null,"tryCatchPattern":"// match on the message to distinguish concurrency refusal from real corruption\nmatch recover(&state_dir, &journal) {\n    Err(e) if e.to_string().contains(\"changed while preparing recovery\") => {\n        // stop editors/sync clients, then retry recovery\n    }\n    Err(e) => return Err(e),\n    Ok(()) => {}\n}","preventionTips":["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"],"tags":["recovery","race-condition","filesystem","concurrency"],"backgroundTag":"invalid-state-transition","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}