{"record":{"id":"3c83169ffc8b5fa2","repo":"astrid-runtime/astrid","slug":"repl-history-path-is-not-a-regular-file","errorCode":null,"errorMessage":"REPL history path is not a regular file: {}","messagePattern":"REPL history path is not a regular file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/repl.rs","lineNumber":116,"sourceCode":"    ///\n    /// Loads command history from the operator-only `log/cli/history` path\n    /// (creating the private file if it does not yet exist) and configures tab\n    /// completion for slash commands. History is never stored in a principal\n    /// home or capsule-visible namespace.\n    pub(crate) fn new() -> anyhow::Result<Self> {\n        let home = astrid_core::dirs::AstridHome::resolve()?;\n        home.ensure()?;\n        let history_dir = home.log_dir().join(\"cli\");\n        astrid_core::platform_fs::ensure_private_directory(&history_dir)?;\n        let history_path = history_dir.join(\"history\");\n        migrate_legacy_history(&history_path, &home.root().join(\"history\"))?;\n\n        // Ensure the history file exists and is a regular, private, no-follow\n        // file so rustyline cannot be redirected through a user-controlled\n        // symlink or special entry.\n        match std::fs::symlink_metadata(&history_path) {\n            Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {\n                anyhow::bail!(\n                    \"REPL history path is not a regular file: {}\",\n                    history_path.display()\n                );\n            },\n            Ok(_) => {\n                astrid_core::platform_fs::verify_no_redirects(&history_path)?;\n                astrid_core::platform_fs::restrict_private_file(&history_path)?;\n            },\n            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {\n                astrid_core::platform_fs::atomic_write_private_file(&history_path, b\"\")?;\n            },\n            Err(error) => return Err(error.into()),\n        }\n\n        let config = Config::builder()\n            .history_ignore_dups(true)?\n            .completion_type(CompletionType::List)\n            .edit_mode(EditMode::Emacs)","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/repl.rs#L98-L134","documentation":"Before rustyline loads the REPL history file, `Repl::new` verifies via symlink_metadata that the path is a real regular file — not a symlink and not a special entry (FIFO, socket, directory, device). This protects the history file from being redirected through a user-controlled symlink or special file, which could leak or corrupt data.","triggerScenarios":"Starting the REPL when the history file path exists as a symlink, a directory, a FIFO/socket/device, or otherwise fails the is_file() check on its symlink metadata.","commonSituations":"A backup/restore tool replaced the history file with a symlink; a dotfile manager (e.g. stow, symlink farms) linked the history path; the path was created as a directory by mistake; a malicious setup planted a special file at the path.","solutions":["Remove the offending entry at the history path (`rm` the symlink/special file).","Create a fresh regular file in its place (the REPL will append to it) or let rustyline create it.","Run the REPL with ASTRID home/history pointing at a clean location.","Check for dotfile managers or restore scripts that symlink this path and adjust them to copy instead."],"exampleFix":"// before\nln -s /mnt/share/bash_history ~/.astrid/history   # symlink -> error\n// after\nrm ~/.astrid/history && touch ~/.astrid/history   # regular file","handlingStrategy":"validation","validationCode":"let history_path = default_history_path();\nmatch std::fs::symlink_metadata(&history_path) {\n    Ok(m) if m.is_file() => Ok(()),\n    Ok(_) | Err(_) => {\n        let _ = std::fs::remove_file(&history_path); // drop symlink/special entry\n        std::fs::File::create(&history_path).map(|_| ())\n    }\n}","typeGuard":"fn is_regular_no_follow(p: &std::path::Path) -> bool {\n    std::fs::symlink_metadata(p)\n        .map(|m| m.is_file())\n        .unwrap_or(false)\n}","tryCatchPattern":"match Repl::new(...) {\n    Ok(repl) => repl,\n    Err(e) if e.to_string().contains(\"not a regular file\") => {\n        eprintln!(\"History path is a symlink/special file; recreating it\");\n        let _ = std::fs::remove_file(&history_path);\n        Repl::new(...)?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Never symlink the REPL history path; dotfile managers should copy it.","Recreate the history file if a restore/backup tool replaced it.","Point the history path at a user-private directory (0700) so others can't plant entries.","Call verify_no_redirects before trusting any user-supplied path."],"tags":["repl","filesystem","symlink","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}