{"record":{"id":"b5b8c9ecbbdab9af","repo":"zeroclaw-labs/zeroclaw","slug":"failed-to-atomically-replace-config-file-e","errorCode":null,"errorMessage":"Failed to atomically replace config file: {e}","messagePattern":"Failed to atomically replace config file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-config/src/schema.rs","lineNumber":22993,"sourceCode":"\n    let had_existing_config = config_path.exists();\n    if had_existing_config {\n        fs::copy(config_path, &backup_path).await.with_context(|| {\n            format!(\n                \"Failed to create config backup before atomic replace: {}\",\n                backup_path.display()\n            )\n        })?;\n    }\n\n    if let Err(e) = fs::rename(&temp_path, config_path).await {\n        let _ = fs::remove_file(&temp_path).await;\n        if had_existing_config && backup_path.exists() {\n            fs::copy(&backup_path, config_path)\n                .await\n                .context(\"Failed to restore config backup\")?;\n        }\n        anyhow::bail!(\"Failed to atomically replace config file: {e}\");\n    }\n\n    #[cfg(unix)]\n    {\n        use std::{fs::Permissions, os::unix::fs::PermissionsExt};\n        if let Err(err) = fs::set_permissions(config_path, Permissions::from_mode(0o600)).await {\n            ::zeroclaw_log::record!(\n                WARN,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Unknown),\n                &format!(\n                    \"Failed to harden config permissions to 0600 at {}: {}\",\n                    config_path.display().to_string(),\n                    err\n                )\n            );\n        }\n    }","sourceCodeStart":22975,"sourceCodeEnd":23011,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-config/src/schema.rs#L22975-L23011","documentation":"write_config_atomically writes the new config to a temp file, backs up the existing config, then publishes via fs::rename(temp, config). This error fires when that final rename fails; the code first removes the temp file and attempts to restore the backup over the config, so the on-disk config should be back to its pre-save state. The {e} suffix carries the underlying io::Error (permission denied, cross-device link, Windows sharing violation, target is a directory, …). Data is not lost — the previous config is preserved or restorable from the .bak file.","triggerScenarios":"Another process holds the config file open with an exclusive lock on Windows (editor, antivirus, sync client like Dropbox/OneDrive) when save()/save_dirty() runs; config_path is on a read-only or full filesystem; the rename crosses a filesystem boundary because the temp file's parent differs from the target's; config_path is actually a directory; insufficient permissions on the parent directory.","commonSituations":"Config auto-save while the user has config.toml open in Excel/locked editor on Windows; config directory inside a synced folder where the sync agent races renames; containers where the config dir is a mounted volume with odd rename semantics; running as a user without write permission on ~/.zeroclaw.","solutions":["Check the {e} suffix: 'permission denied' → fix ownership/permissions of the config path and its parent dir; 'cross-device link' → move temp creation onto the same filesystem (keep config_path local); sharing violation → close editors/AV/sync tools touching the file","Verify free space on the filesystem holding the config","If a .bak file remains next to the config, compare/restore it manually: cp config.toml.bak config.toml","Retry the save once the locking process is gone (the previous config is intact, so this is safe)"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// preflight before save():\nasync fn can_replace(path: &Path) -> bool {\n    let parent = match path.parent() { Some(p) => p, None => return false };\n    std::fs::create_dir_all(parent).is_ok()\n        && path.is_file() || !path.exists()          // not a directory\n        && has_write_access(parent)                  // e.g. try File::create in parent\n}","typeGuard":null,"tryCatchPattern":"let mut attempt = 0;\nloop {\n    attempt += 1;\n    match cfg.save().await {\n        Ok(()) => break,\n        Err(e) if attempt < 3 && e.to_string().contains(\"Failed to atomically replace config file\") => {\n            tokio::time::sleep(std::time::Duration::from_millis(250 * attempt as u64)).await; // Windows lock may clear\n        }\n        Err(e) => {\n            eprintln!(\"config save failed after restore; a .bak may exist: {e:#}\");\n            break;\n        }\n    }\n}","preventionTips":["Keep the config directory out of synced folders (Dropbox/OneDrive) and off network mounts","Don't hold config.toml open in other programs while the app runs auto-save","After any failed save, check for config.toml.bak and restore it before the next start","Ensure the process user owns the config directory and there is free disk space"],"tags":["config","filesystem","atomic-write","rename","io"],"backgroundTag":"atomic-write-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}