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
During recovery validation the library walks every ancestor of the destination path and checks for symlinks via symlink_metadata. If any parent directory has become a symlink since the snapshot was recorded, the recovery is aborted and the parent left untouched, because writing through the symlink could redirect the restore to an attacker- or accident-controlled location. The message signals the snapshot destination's directory tree changed shape and recovery is intentionally skipped.
Solutions
- Inspect the destination's ancestors and replace the symlink with a real directory (or re-point the snapshot) before retrying recovery
- Recreate the original directory layout that existed when the snapshot was taken
- If the symlink is intentional, restore to a new non-symlink absolute path and copy results manually, or re-record the snapshot at the new location
Example fix
// before
# /srv/data -> /mnt/big/data (symlink created after snapshot)
recover_path(Path::new("/srv/data/state"), prior)?; // errors
// after
rm /srv/data && mkdir /srv/data # make parents real directories again
recover_path(Path::new("/srv/data/state"), prior)?; Defensive patterns
Strategy: validation
Validate before calling
use std::fs::symlink_metadata;
fn parents_are_real(p: &std::path::Path) -> bool {
p.ancestors().skip(1).all(|a| {
symlink_metadata(a).map_or(true, |m| !m.is_symlink())
})
} Try / catch
match recover_path(dest, prior) {
Err(e) if e.to_string().contains("now a symlink") => {
eprintln!("resolve the symlinked parent before recovering: {e}");
}
other => other?,
} Prevention
- Avoid replacing snapshot-covered directories with symlinks (move data with bind mounts or config instead)
- Re-checkpoint after any directory-tree restructuring so metadata reflects reality
- Audit ancestor directories with symlink_metadata before running recovery
When it happens
Trigger: Any recover_path/restore attempt where a directory in the destination's ancestor chain (e.g. `/a` in `/a/b/c/file`) is now a symlink, detected with std::fs::symlink_metadata during validate_destination.
Common situations: A user replaced a real directory with a symlink (e.g. moving data to another volume and symlinking); package managers or dotfile managers rewrote directories; tmpfs/var directories got re-pointed between snapshot and recovery.
Related errors
- refusing to follow symlink
- refusing to write dependency sidecar through symlink
- app Info.plist must be a regular file
- binary target is not an owned Caskroom symlink
- brew-cask: refusing elevated operation because target…
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/f0857f7d090f788d.
Report an issue: GitHub.
Appendix: 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 533346cc37)