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
- Simply retry `try_load_or_generate` once: the concurrent writer will have finished writing and the load will succeed.
- Serialize key initialization: create the key once before launching concurrent atuin processes (e.g. run `atuin key` or one init call first).
- Use file locking or an atomic create (O_EXCL create-then-retry) around key generation if you manage the key path yourself.
- 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
- Initialize the encryption key once before launching concurrent atuin processes (shell hooks, services).
- Use a startup lock (flock) around first-run key generation in multi-process environments.
- Never delete or move the key file while atuin sessions are starting.
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
- Could not find history directory
- Could not find history directory
- Could not find history file
- Could not find history file
- default socket path always has a parent
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)