Hmbown/CodeWhale · error · anyhow::Error

config path must not be a symlink: {}

Error message

config path must not be a symlink: {}

What it means

Thrown by reject_path_symlink when the (normalized) config path — the file itself or a parent directory — is a symlink, detected via symlink_metadata so the check does not follow the link. The loader deliberately refuses symlinked config so the exact file that will be read is unambiguous and cannot be swapped by link retargeting.

Source

Thrown at crates/config/src/lib.rs:6743

        .read(true)
        .custom_flags(libc::O_NOFOLLOW)
        .open(path)?;
    let mut raw = String::new();
    file.read_to_string(&mut raw)?;
    Ok(raw)
}

#[cfg(not(unix))]
fn read_string_no_follow(path: &Path) -> std::io::Result<String> {
    fs::read_to_string(path)
}

fn reject_path_symlink(path: &Path) -> Result<()> {
    let Ok(metadata) = fs::symlink_metadata(path) else {
        return Ok(());
    };
    if metadata.file_type().is_symlink() {
        bail!("config path must not be a symlink: {}", path.display());
    }
    Ok(())
}

#[derive(Debug, Clone, Default)]
struct EnvRuntimeOverrides {
    provider: Option<ProviderKind>,
    provider_source: Option<&'static str>,
    model: Option<String>,
    volcengine_model: Option<String>,
    wanjie_ark_model: Option<String>,
    openrouter_model: Option<String>,
    orcarouter_model: Option<String>,
    moonshot_model: Option<String>,
    xiaomi_mimo_model: Option<String>,
    xiaomi_mimo_mode: Option<String>,
    novita_model: Option<String>,
    fireworks_model: Option<String>,

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Replace the symlink with the real file (cp the target into place) and edit it directly
  2. Pass --config pointing at the real target file (absolute, no '..'), bypassing the symlinked location
  3. Reconfigure your dotfile manager to copy instead of link for this file, or use a bind mount on Linux instead of a symlink

Example fix

# before
ln -s ~/shared/codewhale.toml ~/.codewhale/codewhale.toml
codewhale

# after
cp ~/shared/codewhale.toml ~/.codewhale/codewhale.toml
codewhale
Defensive patterns

Strategy: validation

Validate before calling

fn is_clean_config_path(p: &std::path::Path) -> bool {
    fn sym(v: &std::path::Path) -> bool {
        std::fs::symlink_metadata(v).map(|m| m.file_type().is_symlink()).unwrap_or(false)
    }
    if sym(p) { return false; }
    p.ancestors().skip(1).all(|a| !sym(a))
}

Try / catch

match normalize_config_file_path(path) {
    Ok(p) => p,
    Err(err) if err.to_string().contains("must not be a symlink") => {
        // resolve the link ourselves and pass the real target instead
        let real = std::fs::canonicalize(&path)?;
        normalize_config_file_path(real)?
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: ln -s ~/shared/codewhale.toml ~/.config/codewhale/codewhale.toml; a symlinked parent directory (e.g. ~/.config/codewhale -> /mnt/config); dotfile managers (GNU stow), Nix home-manager, or deploy tooling that installs config as links.

Common situations: Sharing one config across machines via a synced/symlinked file; wanting quick config switching by retargeting a link (this is exactly what is being blocked); container setups that bind config through symlinks.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/d74b1cc4ea4b90a8. Report an issue: GitHub.