{"record":{"id":"f0857f7d090f788d","repo":"jdx/mise","slug":"a-parent-directory-is-now-a-symlink-left-untouche","errorCode":null,"errorMessage":"a parent directory is now a symlink; left untouched","messagePattern":"a parent directory is now a symlink; left untouched","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/history/recovery.rs","lineNumber":115,"sourceCode":"                }\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()) {\n        bail!(\"invalid recovery content identifier\");\n    }\n    Ok(())\n}\n\npub(super) fn read_blob(state_dir: &Path, blob: &Blob) -> Result<Vec<u8>> {\n    use base64::Engine;\n    validate_blob_id(&blob.sha256)?;\n    let bytes = match &blob.inline {\n        Some(inline) => base64::engine::general_purpose::STANDARD.decode(inline)?,\n        None => {","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/history/recovery.rs#L97-L133","documentation":"validate_destination walks all ancestors of the recovery destination and refuses to proceed if any of them is now a symlink. A symlinked parent could redirect the restore into a different location than the journal recorded, so recovery bails and leaves the path untouched. This usually means the user (or a tool) restructured directories since the operation ran.","triggerScenarios":"recover_path -> validate_destination finds, via symlink_metadata on each ancestor, that a directory on the path (e.g. `~/.config` or the target's parent) has been replaced with a symlink after the journal entry was written.","commonSituations":"Dotfiles manager changed to symlinked config dirs (e.g. linking `~/.config` into a repo), a fresh machine setup symlinked `~/.cache` or `~/.local`, or a user manually moved a directory and left a symlink behind before running recovery.","solutions":["Remove or replace the symlinked ancestor so the real directory structure matches what existed when the operation ran, then rerun `mise bootstrap dotfiles recover`.","Accept the current file contents with `recover <operation> --keep-current` and reconcile manually.","Re-create the preimage contents manually from your dotfiles repository instead of relying on the sidecar journal."],"exampleFix":"// before\n~/.config -> /home/user/dotfiles/config   # symlinked parent blocks recovery\n\n// after (real directory restored)\n$ rm ~/.config && mkdir ~/.config && cp -a /home/user/dotfiles/config/. ~/.config/\n$ mise bootstrap dotfiles recover","handlingStrategy":"validation","validationCode":"// precheck ancestors for symlinks before running recovery\nfor ancestor in target.ancestors().skip(1) {\n    if let Ok(meta) = std::fs::symlink_metadata(ancestor) {\n        assert!(!meta.is_symlink(), \"{ancestor:?} is a symlink; recovery will refuse\");\n    }\n}","typeGuard":null,"tryCatchPattern":"if let Err(e) = recover(&state_dir, &journal) {\n    if e.to_string().contains(\"parent directory is now a symlink\") {\n        // decide: remove the symlink to restore the recorded layout, or --keep-current\n    }\n}","preventionTips":["Avoid symlinked config directories (e.g. ~/.config -> repo/config) while dotfile write operations are pending recovery","Run recovery immediately after an interrupted operation, before restructuring directories","Check `ls -la` on all ancestors of pending paths when recovery refuses"],"tags":["symlink","filesystem","recovery","safety-check"],"backgroundTag":"invalid-argument-value","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}