{"record":{"id":"f29e20b7baea0ee3","repo":"jdx/mise","slug":"brew-cask-refusing-to-restore-flight-target-throu","errorCode":null,"errorMessage":"brew-cask: refusing to restore flight target through a changed parent: {}","messagePattern":"brew-cask: refusing to restore flight target through a changed parent: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":3447,"sourceCode":"\nfn resolved_parent(path: &Path) -> Result<PathBuf> {\n    let parent = path\n        .parent()\n        .ok_or_else(|| eyre!(\"brew-cask: flight target has no parent\"))?;\n    Ok(path_with_resolved_existing_ancestor(parent))\n}\n\nfn validate_backup_parents(entry: &ArtifactLinkBackup) -> Result<()> {\n    if resolved_parent(&entry.target)? != entry.target_parent\n        || entry\n            .backup\n            .as_deref()\n            .zip(entry.backup_parent.as_ref())\n            .is_some_and(|(backup, expected)| {\n                !resolved_parent(backup).is_ok_and(|current| current == *expected)\n            })\n    {\n        bail!(\n            \"brew-cask: refusing to restore flight target through a changed parent: {}\",\n            entry.target.display()\n        );\n    }\n    Ok(())\n}\n\nfn flight_backup_parent(target: &Path) -> Result<&Path> {\n    if let Some(app) = target.ancestors().find(|ancestor| {\n        ancestor\n            .extension()\n            .is_some_and(|extension| extension.eq_ignore_ascii_case(\"app\"))\n    }) {\n        return app\n            .parent()\n            .ok_or_else(|| eyre!(\"brew-cask: app flight target has no parent\"));\n    }\n    target","sourceCodeStart":3429,"sourceCodeEnd":3465,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/src/system/packages/brew/cask.rs#L3429-L3465","documentation":"validate_backup_parents runs during crash recovery of flight backups. It re-resolves the parent directory of the recorded target — and of the backup, if any — and compares against the parents recorded when the backup was taken (resolved_parent on both sides). A mismatch means the directory now reachable at that path is not the one the backup was made in (replaced, renamed, or reached through a different symlink), so restoring through it is refused.","triggerScenarios":"The target's parent directory was unlinked/recreated (different inode) or a symlinked ancestor such as the appdir or Homebrew prefix was retargeted between the interrupted operation and the recovery run; validate_backup_parents then fails for that ArtifactLinkBackup entry.","commonSituations":"App directory moved or restructured after a crash; switching between /Applications and ~/Applications; prefix symlink changes; overlay/container filesystems remounting parent directories.","solutions":["Restore the parent directory to its original identity — remove replacement symlinks or move the real directory back — then re-run","If the original parent is gone for good, restore the backup file by hand (cp/mv to the desired location) and delete the .recovery record","Re-run the brew command after the parent path is stable","Avoid renaming the appdir or prefix while recovery records are pending"],"exampleFix":"# before: parent replaced by a symlink\n$ ls -ld ~/Applications   -> link to /Volumes/Extra/Applications\n\n# after\n$ rm ~/Applications && mkdir ~/Applications\n$ mise brew install --cask foo   # recovery proceeds","handlingStrategy":"retry","validationCode":"// Before recovery, confirm recorded parents still resolve identically.\nfn parent_still_matches(record: &FlightRecoveryRecord) -> bool {\n    std::fs::canonicalize(record.target.parent().unwrap_or(Path::new(\"/\")))\n        .map(|p| p == record.target_parent)\n        .unwrap_or(false)\n        && record.backup_parent.as_ref().map(|expected| {\n            std::fs::canonicalize(record.backup.as_deref().unwrap().parent().unwrap())\n                .map(|p| p == *expected).unwrap_or(false)\n        }).unwrap_or(true)\n}","typeGuard":null,"tryCatchPattern":"match recover() {\n    Err(e) if e.to_string().contains(\"changed parent\") => {\n        // restore the original parent directory identity, then retry recovery\n        restore_parent_identity();\n        recover()\n    }\n    other => other,\n}","preventionTips":["Don't rename, move, or symlink the appdir/prefix while recovery records are pending","After a crash, stabilize the directory layout before re-running brew commands","If the original parent is unrecoverable, restore backup files by hand and clear the records"],"tags":["brew-cask","recovery","toctou","parent-directory","symlink"],"backgroundTag":"parent-directory-changed","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}