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
- Replace the symlink with a real file (cp --remove-destination the target over the link)
- Point the config-path setting at the real file location instead of the link
- 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
- Configure dotfile managers to copy rather than link for this config
- Check symlink_metadata before pointing the tool at a managed path
- Use bind mounts where the config must live on another volume
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
- project-scope config must not be a symlink
- refusing non-regular or reparse-point config lock at
- agent profile path is not a directory
- audited skill path does not match owned package
- built-in plugin path may not be a symbolic link or reparse…
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)