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

  1. Replace the symlink with a real file (cp --remove-destination of the link target, or rm the link and copy the content).
  2. Put the shared config at the user/global config scope instead of project scope and delete the project-scope link.
  3. 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

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


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)