{"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":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/config/src/lib.rs","lineNumber":6743,"sourceCode":"        .read(true)\n        .custom_flags(libc::O_NOFOLLOW)\n        .open(path)?;\n    let mut raw = String::new();\n    file.read_to_string(&mut raw)?;\n    Ok(raw)\n}\n\n#[cfg(not(unix))]\nfn read_string_no_follow(path: &Path) -> std::io::Result<String> {\n    fs::read_to_string(path)\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":6725,"sourceCodeEnd":6761,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/config/src/lib.rs#L6725-L6761","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Replace the symlink with the real file (cp the target into place) and edit it directly","Pass --config pointing at the real target file (absolute, no '..'), bypassing the symlinked location","Reconfigure your dotfile manager to copy instead of link for this file, or use a bind mount on Linux instead of a symlink"],"exampleFix":"# before\nln -s ~/shared/codewhale.toml ~/.codewhale/codewhale.toml\ncodewhale\n\n# after\ncp ~/shared/codewhale.toml ~/.codewhale/codewhale.toml\ncodewhale","handlingStrategy":"validation","validationCode":"fn is_clean_config_path(p: &std::path::Path) -> bool {\n    fn sym(v: &std::path::Path) -> bool {\n        std::fs::symlink_metadata(v).map(|m| m.file_type().is_symlink()).unwrap_or(false)\n    }\n    if sym(p) { return false; }\n    p.ancestors().skip(1).all(|a| !sym(a))\n}","typeGuard":null,"tryCatchPattern":"match normalize_config_file_path(path) {\n    Ok(p) => p,\n    Err(err) if err.to_string().contains(\"must not be a symlink\") => {\n        // resolve the link ourselves and pass the real target instead\n        let real = std::fs::canonicalize(&path)?;\n        normalize_config_file_path(real)?\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Install config by copy, not symlink, in dotfile managers (stow --no-symlinks style)","Check provisioning scripts: home-manager/Nix installs links by default","Point --config at the real file location directly"],"tags":["rust","config","symlink","filesystem","security"],"backgroundTag":"symlink-rejected","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}