{"record":{"id":"fd9b63c3be6a281b","repo":"atuinsh/atuin","slug":"key-file-vanished-immediately-after-a-concurrent-w","errorCode":null,"errorMessage":"key file vanished immediately after a concurrent write","messagePattern":"key file vanished immediately after a concurrent write","errorType":"exception","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/atuin-common/src/encryption/paseto_v4.rs","lineNumber":364,"sourceCode":"    /// [`Self::generate`], stores it and returns it.\n    pub fn try_load_or_generate(path: &Path) -> Result<Self, KeyFileLoadOrGenerateError> {\n        match Self::try_load_from_path(path) {\n            Ok(s) => Ok(s),\n            Err(KeyFileLoadingError::NoEntry) => {\n                let key = Self::generate();\n                match key.try_write_path(path) {\n                    Ok(()) => Ok(key),\n                    // We lost a race: another process wrote a key between our existence check and\n                    // our write. Adopt whatever landed on disk rather than clobbering it or\n                    // panicking.\n                    Err(KeyFileStoringError::AlreadyExists) => Self::try_load_from_path(path)\n                        .map_err(|e| match e {\n                            KeyFileLoadingError::Io(io) => KeyFileLoadOrGenerateError::Io(io),\n                            KeyFileLoadingError::Decoding(d) => {\n                                KeyFileLoadOrGenerateError::Decoding(d)\n                            }\n                            KeyFileLoadingError::NoEntry => {\n                                KeyFileLoadOrGenerateError::Io(std::io::Error::new(\n                                    std::io::ErrorKind::NotFound,\n                                    \"key file vanished immediately after a concurrent write\",\n                                ))\n                            }\n                        }),\n                    Err(KeyFileStoringError::Io(io)) => Err(io.into()),\n                    Err(KeyFileStoringError::TempFilesExhausted) => {\n                        Err(KeyFileLoadOrGenerateError::TempFilesExhausted)\n                    }\n                }\n            }\n            Err(KeyFileLoadingError::Io(io)) => Err(io.into()),\n            Err(KeyFileLoadingError::Decoding(d)) => Err(d.into()),\n        }\n    }\n\n    /// Get the mnemonic of this particular key.\n    pub fn try_mnemonic(&self) -> Result<bip39::Mnemonic, bip39::ErrorKind> {","sourceCodeStart":346,"sourceCodeEnd":382,"githubUrl":"https://github.com/atuinsh/atuin/blob/900007c48045867e7f44b0f9bc55ad9dbe99ee0d/crates/atuin-common/src/encryption/paseto_v4.rs#L346-L382","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nlet key = Key::try_load_or_generate()?; // panics/fails on rare race\n\n// after\nlet key = match Key::try_load_or_generate() {\n    Ok(k) => k,\n    Err(e) if e.to_string().contains(\"vanished immediately after a concurrent write\") => {\n        std::thread::sleep(std::time::Duration::from_millis(50));\n        Key::try_load_or_generate()? // retry: the concurrent writer has finished\n    }\n    Err(e) => return Err(e),\n};","handlingStrategy":"retry","validationCode":"let key_path = std::path::Path::new(\"~/.local/share/atuin/key\");\nif !key_path.exists() {\n    // initialize key single-threaded before spawning concurrent atuin processes\n}","typeGuard":null,"tryCatchPattern":"match Key::try_load_or_generate() {\n    Ok(k) => k,\n    Err(e) if io_error_kind(&e) == Some(std::io::ErrorKind::NotFound) => {\n        std::thread::sleep(std::time::Duration::from_millis(50));\n        Key::try_load_or_generate().expect(\"key file still missing after retry\")\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"tags":["encryption","filesystem","race-condition","key-management"],"backgroundTag":"file-not-found","analyzedSha":"900007c48045867e7f44b0f9bc55ad9dbe99ee0d","analyzedAt":"2026-09-19T12:52:51.491Z","contentChangedAt":"2026-09-19T12:52:51.491Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}