Hmbown/CodeWhale · error · io::Error
project-scope config must not be a symlink
Error message
project-scope config must not be a symlink
What it means
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.
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.
Example fix
// before ln -s ~/shared/codewhale.toml .codewhale.toml // after cp ~/shared/codewhale.toml .codewhale.toml # real file, not a link
Defensive patterns
Strategy: validation
Validate before calling
let md = std::fs::symlink_metadata(path)?;
if md.file_type().is_symlink() { eprintln!("{path:?} is a symlink; replace it with a real file"); } Type guard
fn is_regular_file(p: &Path) -> bool {
std::fs::symlink_metadata(p).map(|m| m.file_type().is_file()).unwrap_or(false)
} Prevention
- 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).
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- built-in plugin path may not be a symbolic link or reparse…
- CodewhalePet/1
- config path must not be a symlink
- could not securely open
- external credential path must name a non-reparse regular…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/2fc1ac3f4d6758e9.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/lib.rs:11152
"warning: project-scope `instructions` is ignored — \
configure instruction files from user config instead. \
(See #417.)"
);
}
}
/// Maximum bytes read from a project config file. Configs are kilobytes.
const MAX_PROJECT_CONFIG_BYTES: u64 = 1024 * 1024;
fn read_project_config_file(path: &Path) -> io::Result<Option<String>> {
let metadata = match std::fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(err),
};
let file_type = metadata.file_type();
if file_type.is_symlink() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"project-scope config must not be a symlink",
));
}
if !file_type.is_file() {
return Ok(None);
}
let file = open_project_config_file(path)?;
let mut raw = String::new();
file.take(MAX_PROJECT_CONFIG_BYTES + 1)
.read_to_string(&mut raw)?;
if raw.len() as u64 > MAX_PROJECT_CONFIG_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("project config {} exceeds the 1 MiB limit", path.display()),
));
}View on GitHub (pinned to 73e0f67d83)