{"record":{"id":"891717d904188766","repo":"astrid-runtime/astrid","slug":"legacy-principal-home-root-is-not-a-directory-ro","errorCode":null,"errorMessage":"legacy principal-home root is not a directory: {root}","messagePattern":"legacy principal-home root is not a directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/lib.rs","lineNumber":4123,"sourceCode":"    })?;\n    Ok(Arc::new(audit_log))\n}\n\n/// Enforce the released audit-source contract before opening any native\n/// database. The first released layout had one system audit directory under\n/// the `default` principal; ordinary principal-home migration deliberately\n/// excludes every `.local/audit` subtree. An additional non-default source is\n/// therefore a hard migration conflict rather than something that may be\n/// silently left mounted or copied as ordinary home data.\n#[cfg(unix)]\npub(crate) fn preflight_legacy_audit_sources(\n    home: &astrid_core::dirs::AstridHome,\n    default_source: &Path,\n) -> std::io::Result<bool> {\n    let root = home.home_dir();\n    let metadata = match std::fs::symlink_metadata(&root) {\n        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {\n            return Err(std::io::Error::new(\n                std::io::ErrorKind::InvalidData,\n                format!(\n                    \"legacy principal-home root is not a directory: {}\",\n                    root.display()\n                ),\n            ));\n        },\n        Ok(metadata) => metadata,\n        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),\n        Err(error) => return Err(error),\n    };\n    let root_device = audit_tree_device(&metadata);\n    let mut default_source_present = false;\n    astrid_core::platform_fs::verify_no_redirects(&root)?;\n    for entry in std::fs::read_dir(&root)? {\n        let entry = entry?;\n        let principal_root = entry.path();\n        let principal_metadata = std::fs::symlink_metadata(&principal_root)?;","sourceCodeStart":4105,"sourceCodeEnd":4141,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/lib.rs#L4105-L4141","documentation":"A legacy-layout preflight checks that the principal-home root (`home.home_dir()`) is a plain directory. If `symlink_metadata` shows it is a symlink or not a directory, the check fails with InvalidData: legacy homes must be real directories so that redirects/symlinks cannot smuggle secrets or layout state outside the audited tree.","triggerScenarios":"Running the legacy principal-home audit when the home path is a symlink (e.g. `$HOME` resolved through a symlinked dotdir) or a file/mount point rather than a directory.","commonSituations":"Dotfile managers replacing config directories with symlinks (chezmoi, stow, dotbot); home on a symlinked path; a stray file occupying the home path after a bad restore.","solutions":["Replace the symlink with a real directory (move the target's contents into the path and delete the link).","Run the tool with HOME / AstridHome pointing at a real directory, not a symlinked one.","If a file occupies the path, move it aside and create the directory.","Re-run the legacy audit after the path is a plain directory."],"exampleFix":"// before: home path is a symlink\n~/.astrid -> /mnt/data/astrid\n// after\nmv ~/.astrid ~/astrid-real && rm ~/.astrid\nmv ~/astrid-real ~/.astrid  # real directory, no symlink","handlingStrategy":"validation","validationCode":"let root = home.home_dir();\nlet md = std::fs::symlink_metadata(&root)?;\nif md.file_type().is_symlink() || !md.is_dir() {\n    return Err(format!(\"{} must be a real directory, not a symlink/file\", root.display()));\n}","typeGuard":"fn is_plain_dir(p: &std::path::Path) -> bool {\n    match std::fs::symlink_metadata(p) {\n        Ok(md) => !md.file_type().is_symlink() && md.is_dir(),\n        Err(_) => false,\n    }\n}","tryCatchPattern":"match run_legacy_home_audit(&home) {\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData && e.to_string().contains(\"not a directory\") => {\n        eprintln!(\"replace the symlink with a real directory, then retry\");\n    }\n    other => other?,\n}","preventionTips":["Do not let dotfile managers symlink the Astrid home directory itself.","Run the audit only when home paths are plain directories.","Check with `ls -la` (or symlink_metadata) before pointing the kernel at a home path."],"tags":["migration","symlink","filesystem","security"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}