jdx/mise · error

a parent directory is now a symlink; left untouched

Error message

a parent directory is now a symlink; left untouched

What it means

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.

Source

Thrown at src/system/history/recovery.rs:115

                }
            ))
    {
        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()) {
        bail!("invalid recovery content identifier");
    }
    Ok(())
}

pub(super) fn read_blob(state_dir: &Path, blob: &Blob) -> Result<Vec<u8>> {
    use base64::Engine;
    validate_blob_id(&blob.sha256)?;
    let bytes = match &blob.inline {
        Some(inline) => base64::engine::general_purpose::STANDARD.decode(inline)?,
        None => {

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Remove or replace the symlinked ancestor so the real directory structure matches what existed when the operation ran, then rerun `mise bootstrap dotfiles recover`.
  2. Accept the current file contents with `recover <operation> --keep-current` and reconcile manually.
  3. Re-create the preimage contents manually from your dotfiles repository instead of relying on the sidecar journal.

Example fix

// before
~/.config -> /home/user/dotfiles/config   # symlinked parent blocks recovery

// after (real directory restored)
$ rm ~/.config && mkdir ~/.config && cp -a /home/user/dotfiles/config/. ~/.config/
$ mise bootstrap dotfiles recover
Defensive patterns

Strategy: validation

Validate before calling

// precheck ancestors for symlinks before running recovery
for ancestor in target.ancestors().skip(1) {
    if let Ok(meta) = std::fs::symlink_metadata(ancestor) {
        assert!(!meta.is_symlink(), "{ancestor:?} is a symlink; recovery will refuse");
    }
}

Try / catch

if let Err(e) = recover(&state_dir, &journal) {
    if e.to_string().contains("parent directory is now a symlink") {
        // decide: remove the symlink to restore the recorded layout, or --keep-current
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/f0857f7d090f788d. Report an issue: GitHub.