{"record":{"id":"2fc1ac3f4d6758e9","repo":"Hmbown/CodeWhale","slug":"project-scope-config-must-not-be-a-symlink","errorCode":null,"errorMessage":"project-scope config must not be a symlink","messagePattern":"project-scope config must not be a symlink","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/lib.rs","lineNumber":11152,"sourceCode":"            \"warning: project-scope `instructions` is ignored — \\\n             configure instruction files from user config instead. \\\n             (See #417.)\"\n        );\n    }\n}\n\n/// Maximum bytes read from a project config file. Configs are kilobytes.\nconst MAX_PROJECT_CONFIG_BYTES: u64 = 1024 * 1024;\n\nfn read_project_config_file(path: &Path) -> io::Result<Option<String>> {\n    let metadata = match std::fs::symlink_metadata(path) {\n        Ok(metadata) => metadata,\n        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),\n        Err(err) => return Err(err),\n    };\n    let file_type = metadata.file_type();\n    if file_type.is_symlink() {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidInput,\n            \"project-scope config must not be a symlink\",\n        ));\n    }\n    if !file_type.is_file() {\n        return Ok(None);\n    }\n\n    let file = open_project_config_file(path)?;\n    let mut raw = String::new();\n    file.take(MAX_PROJECT_CONFIG_BYTES + 1)\n        .read_to_string(&mut raw)?;\n    if raw.len() as u64 > MAX_PROJECT_CONFIG_BYTES {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\"project config {} exceeds the 1 MiB limit\", path.display()),\n        ));\n    }","sourceCodeStart":11134,"sourceCodeEnd":11170,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/lib.rs#L11134-L11170","documentation":"The project-scope config loader refuses to read a config file that is a symbolic link. Symlinked project configs are rejected because a link could point outside the project directory, letting a repo redirect config reads to an arbitrary file (config-injection/TOCTOU risk). The check runs right after metadata is fetched; if the path itself is missing, Ok(None) is returned instead.","triggerScenarios":"Calling the project-config loader when the config file at the resolved project path is a symlink (metadata.file_type().is_symlink() is true). Happens regardless of what the link points to, even inside the project.","commonSituations":"Developers symlinking a shared dotfile (e.g. ~/.config-shared/codewhale.toml) into a project checkout; dotfile managers (stow, chezmoi) replacing config files with links; CI checkouts where a symlink was committed to the repo.","solutions":["Replace the symlink with a real file (cp --remove-destination of the link target, or rm the link and copy the content).","Put the shared config at the user/global config scope instead of project scope and delete the project-scope link.","If the link was committed, remove it from the repository and commit the real file."],"exampleFix":"// before\nln -s ~/shared/codewhale.toml .codewhale.toml\n// after\ncp ~/shared/codewhale.toml .codewhale.toml   # real file, not a link","handlingStrategy":"validation","validationCode":"let md = std::fs::symlink_metadata(path)?;\nif md.file_type().is_symlink() { eprintln!(\"{path:?} is a symlink; replace it with a real file\"); }","typeGuard":"fn is_regular_file(p: &Path) -> bool {\n    std::fs::symlink_metadata(p).map(|m| m.file_type().is_file()).unwrap_or(false)\n}","tryCatchPattern":null,"preventionTips":["Never symlink project-scope config files; commit real files.","Configure dotfile managers to skip project directories.","Add a CI check that fails if committed config paths are symlinks (git stores them as mode 120000)."],"tags":["config","symlink","filesystem","security"],"backgroundTag":"invalid-config-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"}