atuinsh/atuin · error · std::io::Error

key file vanished immediately after a concurrent write

Error message

key file vanished immediately after a concurrent write

What it means

In `Key::try_load_or_generate`, when the key file is loaded after being generated by another thread/process, the loader reports `KeyFileLoadingError::NoEntry`. Since at that point the file was expected to exist (a concurrent writer just created it), the code converts NoEntry into an io::Error with kind NotFound and the message "key file vanished immediately after a concurrent write". It signals a race between generating and re-reading the key file, not a genuinely missing key.

Solutions

  1. Simply retry `try_load_or_generate` once: the concurrent writer will have finished writing and the load will succeed.
  2. Serialize key initialization: create the key once before launching concurrent atuin processes (e.g. run `atuin key` or one init call first).
  3. Use file locking or an atomic create (O_EXCL create-then-retry) around key generation if you manage the key path yourself.
  4. If the file is genuinely absent and stays absent, fall back to the normal load/generate path again — a persistent NotFound means the key was really deleted and a new key will be generated (breaking sync until re-keyed).

Example fix

// before
let key = Key::try_load_or_generate()?; // panics/fails on rare race

// after
let key = match Key::try_load_or_generate() {
    Ok(k) => k,
    Err(e) if e.to_string().contains("vanished immediately after a concurrent write") => {
        std::thread::sleep(std::time::Duration::from_millis(50));
        Key::try_load_or_generate()? // retry: the concurrent writer has finished
    }
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: retry

Validate before calling

let key_path = std::path::Path::new("~/.local/share/atuin/key");
if !key_path.exists() {
    // initialize key single-threaded before spawning concurrent atuin processes
}

Try / catch

match Key::try_load_or_generate() {
    Ok(k) => k,
    Err(e) if io_error_kind(&e) == Some(std::io::ErrorKind::NotFound) => {
        std::thread::sleep(std::time::Duration::from_millis(50));
        Key::try_load_or_generate().expect("key file still missing after retry")
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Two processes/threads call `Key::try_load_or_generate` simultaneously against the same key path (e.g. ~/.local/share/atuin/key): one generates and writes the file, the other's re-read hits NoEntry between check and read. Also occurs if the key file is deleted by another agent (security tooling, tmpfs cleanup) in the window between existence check and open.

Common situations: Running multiple atuin instances at startup (shell hooks in several terminals launching at once), container startup scripts racing, or a key file on a volatile filesystem where another process removes it.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of atuinsh/atuin@900007c480 (2026-09-19). Data as JSON: /api/errors/fd9b63c3be6a281b. Report an issue: GitHub.

Appendix: source

Thrown at crates/atuin-common/src/encryption/paseto_v4.rs:364

    /// [`Self::generate`], stores it and returns it.
    pub fn try_load_or_generate(path: &Path) -> Result<Self, KeyFileLoadOrGenerateError> {
        match Self::try_load_from_path(path) {
            Ok(s) => Ok(s),
            Err(KeyFileLoadingError::NoEntry) => {
                let key = Self::generate();
                match key.try_write_path(path) {
                    Ok(()) => Ok(key),
                    // We lost a race: another process wrote a key between our existence check and
                    // our write. Adopt whatever landed on disk rather than clobbering it or
                    // panicking.
                    Err(KeyFileStoringError::AlreadyExists) => Self::try_load_from_path(path)
                        .map_err(|e| match e {
                            KeyFileLoadingError::Io(io) => KeyFileLoadOrGenerateError::Io(io),
                            KeyFileLoadingError::Decoding(d) => {
                                KeyFileLoadOrGenerateError::Decoding(d)
                            }
                            KeyFileLoadingError::NoEntry => {
                                KeyFileLoadOrGenerateError::Io(std::io::Error::new(
                                    std::io::ErrorKind::NotFound,
                                    "key file vanished immediately after a concurrent write",
                                ))
                            }
                        }),
                    Err(KeyFileStoringError::Io(io)) => Err(io.into()),
                    Err(KeyFileStoringError::TempFilesExhausted) => {
                        Err(KeyFileLoadOrGenerateError::TempFilesExhausted)
                    }
                }
            }
            Err(KeyFileLoadingError::Io(io)) => Err(io.into()),
            Err(KeyFileLoadingError::Decoding(d)) => Err(d.into()),
        }
    }

    /// Get the mnemonic of this particular key.
    pub fn try_mnemonic(&self) -> Result<bip39::Mnemonic, bip39::ErrorKind> {

View on GitHub (pinned to 900007c480)