{"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":278,"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                }\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> {\n        bip39::Mnemonic::from_entropy(self.as_bytes(), bip39::Language::English)\n    }\n","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/atuinsh/atuin/blob/202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1/crates/atuin-common/src/encryption/paseto_v4.rs#L260-L296","documentation":"Produced by Key::try_load_or_generate in atuin-common's PASETO V4 key handling. The sequence is: the key file did not exist (NoEntry), a fresh key was generated, try_write_path refused because a file appeared concurrently (AlreadyExists), and the follow-up try_load_from_path then found the file gone again (NoEntry). That double-disappearance is mapped to io::ErrorKind::NotFound with this message. It signals an extremely tight race where another process (or an external deleter) is creating and removing the key file between syscalls.","triggerScenarios":"Two Atuin processes on first run hitting try_load_or_generate for the same path at the same instant, combined with something deleting the file in between; a filesystem watcher, sync client, or antivirus quarantining the freshly created key file; a network filesystem with create/delete visibility lag.","commonSituations":"First-run races between the atuin daemon and the CLI in fresh environments; dotfile-sync tools (Dropbox, Syncthing) managing ~/.local/share/atuin and conflicting with key creation; security software removing unknown key files; tests that run many atuin processes in parallel against a shared HOME.","solutions":["Retry try_load_or_generate once or twice — the race window is nanoseconds-wide and a retry almost always succeeds","Identify and stop the concurrent deleter: check security software / sync clients that manage the data directory","Pre-create the key file on one process (e.g. run `atuin key` or a single registration step) before starting parallel consumers","Verify the data directory is on a local filesystem, not a flaky network mount"],"exampleFix":"// before\nlet key = Key::try_load_or_generate(&path)?;\n\n// after\nlet key = Key::try_load_or_generate(&path)\n    .or_else(|_| Key::try_load_or_generate(&path))?;","handlingStrategy":"retry","validationCode":"// Serialize first-run key creation across processes with a lockfile\nlet lock = fs4::FileExt::try_lock_exclusive(\n    &mut std::fs::File::create(path.with_extension(\"lock\"))?,\n)?; // hold while calling try_load_or_generate","typeGuard":null,"tryCatchPattern":"let mut attempts = 0;\nlet key = loop {\n    attempts += 1;\n    match Key::try_load_or_generate(&path) {\n        Ok(k) => break k,\n        Err(e) if attempts < 3 => continue, // race window is tiny; retry wins\n        Err(e) => return Err(e.into()),\n    }\n};","preventionTips":["Pre-create the key file once (single registration step) before starting many parallel Atuin processes","Keep the Atuin data directory out of sync-tool managed folders (Dropbox/Syncthing) that may create/delete files","Exclude the key file from antivirus/security scanning interference","Use a lockfile around first-run key creation in custom tooling"],"tags":["encryption","paseto","race-condition","filesystem","rust","atuin"],"backgroundTag":"file-exists-race-condition","analyzedSha":"202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1","analyzedAt":"2026-08-16T19:30:24.731Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}