{"record":{"id":"d08da18429768fef","repo":"zeroclaw-labs/zeroclaw","slug":"key-file-path-is-a-symlink-refusing-to-write","errorCode":null,"errorMessage":"Key file path is a symlink — refusing to write","messagePattern":"Key file path is a symlink — refusing to write","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-config/src/secrets.rs","lineNumber":653,"sourceCode":"\n/// Core of atomic key publication.  `write_fn` performs the entire\n/// write-then-durable stage (write_all + flush + sync_all) on the temp file;\n/// extracting it behind a closure lets tests inject a deterministic write-stage\n/// failure and assert that `TempFileGuard` removes the temp file on every early\n/// return.  The temp creation, guard arm, and closure run identically on all\n/// platforms — only the publication step below is `cfg`-split.\nfn write_key_file_atomic_publish_with<F>(key_path: &Path, key: &[u8], write_fn: F) -> Result<()>\nwhere\n    F: FnOnce(&mut std::fs::File, &[u8]) -> std::io::Result<()>,\n{\n    // Ensure parent directory exists.\n    if let Some(parent) = key_path.parent() {\n        fs::create_dir_all(parent)?;\n    }\n\n    // Reject symlink / reparse point on the final path before publishing.\n    if is_symlink_like(key_path) {\n        anyhow::bail!(\"Key file path is a symlink — refusing to write\");\n    }\n\n    // Write full key material to a unique temporary file.  The guard is\n    // armed ONLY after successful creation — arming before create_new would\n    // let a name-collision loser delete another process's temp file.\n    let temp_path = temp_path_for(key_path);\n\n    let mut open_opts = std::fs::OpenOptions::new();\n    open_opts.write(true).create_new(true);\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::OpenOptionsExt;\n        open_opts.mode(0o600); // restrictive at birth\n    }\n    let mut file = open_opts\n        .open(&temp_path)\n        .with_context(|| format!(\"Failed to create temp key file at {}\", temp_path.display()))?;\n","sourceCodeStart":635,"sourceCodeEnd":671,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-config/src/secrets.rs#L635-L671","documentation":"Before atomically publishing a master key file, write_key_file_atomic_publish_with checks the final path with is_symlink_like and refuses to publish if it is a symlink (or Windows reparse point). This is a deliberate symlink-attack defense: writing through a symlink would let the key material land at an attacker-chosen location or be read through a link the writer did not create. Publication is fail-closed — no key bytes are written to the final path.","triggerScenarios":"~/.zeroclaw/.secret_key (or the configured key path) is a symlink to another file, e.g. created with `ln -s ~/shared/key ~/.zeroclaw/.secret_key` to share one key across installs or point at a password-manager-mounted file; a dotfiles manager symlinked the whole ~/.zeroclaw directory's files; on Windows, the path is a reparse point/junction target.","commonSituations":"Users symlinking the key into a synced/backup folder; multi-instance setups sharing one key via symlink; container images that symlink config homes; migrating an old layout by leaving links behind.","solutions":["Replace the symlink with a real file: remove the link, then copy the actual key material to the path (`cp -L` the target into place) or let ZeroClaw generate a fresh key there","If you need one shared key across instances, copy it to each key path instead of symlinking","Check for a symlinked parent-directory situation too (path components that are links still resolve, but a direct link on the final name is the blocker)"],"exampleFix":"# before\nls -l ~/.zeroclaw/.secret_key\n# ~/.zeroclaw/.secret_key -> /mnt/shared/zeroclaw.key\n\n# after\nrm ~/.zeroclaw/.secret_key\ncp /mnt/shared/zeroclaw.key ~/.zeroclaw/.secret_key\nchmod 600 ~/.zeroclaw/.secret_key","handlingStrategy":"validation","validationCode":"fn key_path_writable(path: &Path) -> bool {\n    match std::fs::symlink_metadata(path) {\n        Ok(md) => !md.file_type().is_symlink(), // refuse links, accept regular files/absent\n        Err(_) => true, // absent path is fine\n    }\n}\n// check before first run:\nif !key_path_writable(&key_path) {\n    anyhow::bail!(\"remove the symlink at {} first\", key_path.display());\n}","typeGuard":null,"tryCatchPattern":"if let Err(e) = write_key_file_atomic_publish(&path, &key) {\n    if e.to_string().contains(\"symlink\") {\n        eprintln!(\"{} is a symlink — replace it with a real file and retry\", path.display());\n    }\n    return Err(e);\n}","preventionTips":["Never share master keys via symlinks; copy the file (mode 0600) to each install","Watch out for dotfiles managers that symlink ~/.zeroclaw contents — exclude the key file","After moving/restoring home dirs, check `ls -la ~/.zeroclaw/` for stale links before first run"],"tags":["secrets","key-management","symlink","security","filesystem"],"backgroundTag":"symlink-security-check","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}