{"record":{"id":"75525848ac941166","repo":"astrid-runtime/astrid","slug":"legacy-secrets-root-is-not-a-regular-directory","errorCode":null,"errorMessage":"legacy secrets root is not a regular directory: {}","messagePattern":"legacy secrets root is not a regular directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs","lineNumber":89,"sourceCode":"    fs::remove_dir(path).map_err(io::Error::other)?;\n    sync_parent(path)\n}\n\n/// Check every alias-keyed child of the released `secrets/` root.  The\n/// barrier passes `allow_empty_cleanup=false` while resuming a completed\n/// ledger, so a deleted or renamed principal cannot leave a reappeared empty\n/// directory that is silently swept on restart.\npub(super) fn ensure_legacy_secret_aliases(\n    root: &Path,\n    allow_empty_cleanup: bool,\n) -> io::Result<()> {\n    let metadata = match fs::symlink_metadata(root) {\n        Ok(metadata) => metadata,\n        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),\n        Err(error) => return Err(error),\n    };\n    if metadata.file_type().is_symlink() || !metadata.is_dir() {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\n                \"legacy secrets root is not a regular directory: {}\",\n                root.display()\n            ),\n        ));\n    }\n    astrid_core::platform_fs::validate_private_directory(root)?;\n    astrid_core::platform_fs::verify_no_redirects(root)?;\n    let entries = fs::read_dir(root)\n        .map_err(io::Error::other)?\n        .collect::<Result<Vec<_>, _>>()\n        .map_err(io::Error::other)?;\n    for entry in entries {\n        if entry.file_name() == \"__host__\" {\n            continue;\n        }\n        let path = entry.path();","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs#L71-L107","documentation":"`ensure_legacy_secret_aliases` validates the legacy secrets root before creating aliases. If the root exists but is a symlink or not a directory, the function fails with `InvalidData` rather than aliasing into an unverified location.","triggerScenarios":"Calling `ensure_legacy_secret_aliases` when `symlink_metadata(root)` succeeds but `is_symlink()` is true or `is_dir()` is false.","commonSituations":"The whole legacy secrets directory was symlinked (dotfiles manager, backup restore creating symlinks); a file overwrote the secrets root path.","solutions":["Replace the symlink/file with a real directory containing the legacy secret files.","Recreate the legacy secrets root at its expected path and re-run migration.","Exclude the secrets root from symlink-managing tools."],"exampleFix":"# before\nsecrets -> ~/dotfiles/secrets\n# after\nrm secrets && mkdir secrets && cp -L ~/dotfiles/secrets/* secrets/","handlingStrategy":"validation","validationCode":"fn secrets_root_ok(root: &std::path::Path) -> bool {\n    match std::fs::symlink_metadata(root) {\n        Ok(m) => m.is_dir() && !m.file_type().is_symlink(),\n        Err(e) => e.kind() == std::io::ErrorKind::NotFound,\n    }\n}","typeGuard":"fn is_real_dir_or_missing(p: &std::path::Path) -> bool {\n    !std::path::Path::new(p).symlink_metadata().map(|m| m.file_type().is_symlink() || !m.is_dir()).unwrap_or(false)\n}","tryCatchPattern":"match ensure_legacy_secret_aliases(root, ...) {\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData\n        && e.to_string().contains(\"not a regular directory\") => {\n        eprintln!(\"recreate the legacy secrets root as a real directory\");\n        return Err(e.into());\n    }\n    other => other,\n}","preventionTips":["Verify the secrets root with `ls -la` (no `l` prefix) before migration.","Recreate symlinked roots as real directories with copied content.","Document that the legacy secrets root must never be symlinked."],"tags":["filesystem","symlink","migration"],"backgroundTag":"incompatible-source-type","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"}