Hmbown/CodeWhale · error

config path must not be a symlink

Error message

config path must not be a symlink: {}

What it means

reject_path_symlink checks symlink_metadata and rejects any config path that is itself a symlink. This prevents a user (or attacker) from redirecting config reads/writes through a link to an unintended location. Note it inspects only the final component; if the path does not exist, it passes through.

Solutions

  1. Replace the symlink with a real file (cp --remove-destination the target over the link)
  2. Point the config-path setting at the real file location instead of the link
  3. Use a bind mount instead of a symlink if the config must live elsewhere

Example fix

// before
ln -s ~/dotfiles/config.toml ~/.config/codewhale/config.toml
// after
cp --remove-destination ~/dotfiles/config.toml ~/.config/codewhale/config.toml
Defensive patterns

Strategy: validation

Validate before calling

fn not_a_symlink(p: &std::path::Path) -> bool {
    std::fs::symlink_metadata(p)
        .map(|m| !m.file_type().is_symlink())
        .unwrap_or(true) // nonexistent paths pass, matching the library
}

Type guard

fn real_file(p: &std::path::Path) -> Option<std::path::PathBuf> {
    match std::fs::symlink_metadata(p) {
        Ok(m) if m.file_type().is_symlink() => None,
        Ok(_) => Some(p.to_path_buf()),
        Err(_) => Some(p.to_path_buf()),
    }
}

Try / catch

match normalize_config_file_path(&path) {
    Ok(p) => p,
    Err(e) if e.to_string().contains("must not be a symlink") => {
        eprintln!("replace the symlink with a real file: {e}");
        std::process::exit(2);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Passing a config path where the file (or directory) at that path is a symlink — e.g. config.toml -> /mnt/usb/config.toml — to any API that normalizes the path.

Common situations: Dotfile managers (stow, GNU dotfiles, chezmoi) that symlink config into a repo; containers mounting the config as a symlink; users linking a shared config across machines.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/d74b1cc4ea4b90a8. Report an issue: GitHub.

Appendix: source

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

    let file = fs::File::open(path)?;
    let mut raw = String::new();
    file.take(MAX_CONFIG_FILE_BYTES + 1)
        .read_to_string(&mut raw)?;
    if raw.len() as u64 > MAX_CONFIG_FILE_BYTES {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!("config file {} exceeds the 1 MiB limit", path.display()),
        ));
    }
    Ok(raw)
}

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 73e0f67d83)