zeroclaw-labs/zeroclaw · error
Key file path is a symlink — refusing to write
Error message
Key file path is a symlink — refusing to write
What it means
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.
Source
Thrown at crates/zeroclaw-config/src/secrets.rs:653
/// Core of atomic key publication. `write_fn` performs the entire
/// write-then-durable stage (write_all + flush + sync_all) on the temp file;
/// extracting it behind a closure lets tests inject a deterministic write-stage
/// failure and assert that `TempFileGuard` removes the temp file on every early
/// return. The temp creation, guard arm, and closure run identically on all
/// platforms — only the publication step below is `cfg`-split.
fn write_key_file_atomic_publish_with<F>(key_path: &Path, key: &[u8], write_fn: F) -> Result<()>
where
F: FnOnce(&mut std::fs::File, &[u8]) -> std::io::Result<()>,
{
// Ensure parent directory exists.
if let Some(parent) = key_path.parent() {
fs::create_dir_all(parent)?;
}
// Reject symlink / reparse point on the final path before publishing.
if is_symlink_like(key_path) {
anyhow::bail!("Key file path is a symlink — refusing to write");
}
// Write full key material to a unique temporary file. The guard is
// armed ONLY after successful creation — arming before create_new would
// let a name-collision loser delete another process's temp file.
let temp_path = temp_path_for(key_path);
let mut open_opts = std::fs::OpenOptions::new();
open_opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
open_opts.mode(0o600); // restrictive at birth
}
let mut file = open_opts
.open(&temp_path)
.with_context(|| format!("Failed to create temp key file at {}", temp_path.display()))?;
View on GitHub (pinned to 88bb9c8533)
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)
Example fix
# before ls -l ~/.zeroclaw/.secret_key # ~/.zeroclaw/.secret_key -> /mnt/shared/zeroclaw.key # after rm ~/.zeroclaw/.secret_key cp /mnt/shared/zeroclaw.key ~/.zeroclaw/.secret_key chmod 600 ~/.zeroclaw/.secret_key
Defensive patterns
Strategy: validation
Validate before calling
fn key_path_writable(path: &Path) -> bool {
match std::fs::symlink_metadata(path) {
Ok(md) => !md.file_type().is_symlink(), // refuse links, accept regular files/absent
Err(_) => true, // absent path is fine
}
}
// check before first run:
if !key_path_writable(&key_path) {
anyhow::bail!("remove the symlink at {} first", key_path.display());
} Try / catch
if let Err(e) = write_key_file_atomic_publish(&path, &key) {
if e.to_string().contains("symlink") {
eprintln!("{} is a symlink — replace it with a real file and retry", path.display());
}
return Err(e);
} Prevention
- 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
When it happens
Trigger: ~/.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.
Common situations: 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.
Related errors
- attachment path {} canonicalizes to {} which escapes workspa
- Skill path escapes skills directory: {name}
- Key file path is a symlink — refusing to read
- attachment path {} escapes workspace {}
- The '{}' backend does not support automatic key generation.
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/d08da18429768fef.
Report an issue: GitHub.