{"record":{"id":"d74b1cc4ea4b90a8","repo":"Hmbown/CodeWhale","slug":"config-path-must-not-be-a-symlink","errorCode":null,"errorMessage":"config path must not be a symlink: {}","messagePattern":"config path must not be a symlink: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/config/src/lib.rs","lineNumber":6908,"sourceCode":"    let file = fs::File::open(path)?;\n    let mut raw = String::new();\n    file.take(MAX_CONFIG_FILE_BYTES + 1)\n        .read_to_string(&mut raw)?;\n    if raw.len() as u64 > MAX_CONFIG_FILE_BYTES {\n        return Err(std::io::Error::new(\n            std::io::ErrorKind::InvalidData,\n            format!(\"config file {} exceeds the 1 MiB limit\", path.display()),\n        ));\n    }\n    Ok(raw)\n}\n\nfn reject_path_symlink(path: &Path) -> Result<()> {\n    let Ok(metadata) = fs::symlink_metadata(path) else {\n        return Ok(());\n    };\n    if metadata.file_type().is_symlink() {\n        bail!(\"config path must not be a symlink: {}\", path.display());\n    }\n    Ok(())\n}\n\n#[derive(Debug, Clone, Default)]\nstruct EnvRuntimeOverrides {\n    provider: Option<ProviderKind>,\n    provider_source: Option<&'static str>,\n    model: Option<String>,\n    volcengine_model: Option<String>,\n    wanjie_ark_model: Option<String>,\n    openrouter_model: Option<String>,\n    orcarouter_model: Option<String>,\n    moonshot_model: Option<String>,\n    xiaomi_mimo_model: Option<String>,\n    xiaomi_mimo_mode: Option<String>,\n    novita_model: Option<String>,\n    fireworks_model: Option<String>,","sourceCodeStart":6890,"sourceCodeEnd":6926,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/config/src/lib.rs#L6890-L6926","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nln -s ~/dotfiles/config.toml ~/.config/codewhale/config.toml\n// after\ncp --remove-destination ~/dotfiles/config.toml ~/.config/codewhale/config.toml","handlingStrategy":"validation","validationCode":"fn not_a_symlink(p: &std::path::Path) -> bool {\n    std::fs::symlink_metadata(p)\n        .map(|m| !m.file_type().is_symlink())\n        .unwrap_or(true) // nonexistent paths pass, matching the library\n}","typeGuard":"fn real_file(p: &std::path::Path) -> Option<std::path::PathBuf> {\n    match std::fs::symlink_metadata(p) {\n        Ok(m) if m.file_type().is_symlink() => None,\n        Ok(_) => Some(p.to_path_buf()),\n        Err(_) => Some(p.to_path_buf()),\n    }\n}","tryCatchPattern":"match normalize_config_file_path(&path) {\n    Ok(p) => p,\n    Err(e) if e.to_string().contains(\"must not be a symlink\") => {\n        eprintln!(\"replace the symlink with a real file: {e}\");\n        std::process::exit(2);\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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"],"tags":["config","path","symlink","security"],"backgroundTag":"invalid-argument-value","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}