astrid-runtime/astrid · error

REPL history path is not a regular file: {}

Error message

REPL history path is not a regular file: {}

What it means

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.

Source

Thrown at crates/astrid-cli/src/repl.rs:116

    ///
    /// Loads command history from the operator-only `log/cli/history` path
    /// (creating the private file if it does not yet exist) and configures tab
    /// completion for slash commands. History is never stored in a principal
    /// home or capsule-visible namespace.
    pub(crate) fn new() -> anyhow::Result<Self> {
        let home = astrid_core::dirs::AstridHome::resolve()?;
        home.ensure()?;
        let history_dir = home.log_dir().join("cli");
        astrid_core::platform_fs::ensure_private_directory(&history_dir)?;
        let history_path = history_dir.join("history");
        migrate_legacy_history(&history_path, &home.root().join("history"))?;

        // Ensure the history file exists and is a regular, private, no-follow
        // file so rustyline cannot be redirected through a user-controlled
        // symlink or special entry.
        match std::fs::symlink_metadata(&history_path) {
            Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
                anyhow::bail!(
                    "REPL history path is not a regular file: {}",
                    history_path.display()
                );
            },
            Ok(_) => {
                astrid_core::platform_fs::verify_no_redirects(&history_path)?;
                astrid_core::platform_fs::restrict_private_file(&history_path)?;
            },
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
                astrid_core::platform_fs::atomic_write_private_file(&history_path, b"")?;
            },
            Err(error) => return Err(error.into()),
        }

        let config = Config::builder()
            .history_ignore_dups(true)?
            .completion_type(CompletionType::List)
            .edit_mode(EditMode::Emacs)

View on GitHub (pinned to affd8760f4)

Solutions

  1. Remove the offending entry at the history path (`rm` the symlink/special file).
  2. Create a fresh regular file in its place (the REPL will append to it) or let rustyline create it.
  3. Run the REPL with ASTRID home/history pointing at a clean location.
  4. Check for dotfile managers or restore scripts that symlink this path and adjust them to copy instead.

Example fix

// before
ln -s /mnt/share/bash_history ~/.astrid/history   # symlink -> error
// after
rm ~/.astrid/history && touch ~/.astrid/history   # regular file
Defensive patterns

Strategy: validation

Validate before calling

let history_path = default_history_path();
match std::fs::symlink_metadata(&history_path) {
    Ok(m) if m.is_file() => Ok(()),
    Ok(_) | Err(_) => {
        let _ = std::fs::remove_file(&history_path); // drop symlink/special entry
        std::fs::File::create(&history_path).map(|_| ())
    }
}

Type guard

fn is_regular_no_follow(p: &std::path::Path) -> bool {
    std::fs::symlink_metadata(p)
        .map(|m| m.is_file())
        .unwrap_or(false)
}

Try / catch

match Repl::new(...) {
    Ok(repl) => repl,
    Err(e) if e.to_string().contains("not a regular file") => {
        eprintln!("History path is a symlink/special file; recreating it");
        let _ = std::fs::remove_file(&history_path);
        Repl::new(...)?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/3c83169ffc8b5fa2. Report an issue: GitHub.