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
- 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.
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
- 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
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
- cannot write task stub because {} is a symbolic link
- refusing to resolve managed directory {} to the filesystem r
- brew-cask: refusing to restore flight target through a chang
- failed to create file symlink: {err}
- too many symlinks while resolving atomic write target: {}
AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09).
Data as JSON: /api/errors/f0857f7d090f788d.
Report an issue: GitHub.