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

  1. 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
  2. If you need one shared key across instances, copy it to each key path instead of symlinking
  3. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/d08da18429768fef. Report an issue: GitHub.