Hmbown/CodeWhale · error

MCP config path must be a regular file: {}

Error message

MCP config path must be a regular file: {}

What it means

read_mcp_config_file stats the config path with symlink_metadata and rejects anything that is not a plain regular file: symlinks (even to regular files), directories, FIFOs, and device nodes all fail. This blocks symlink-swap attacks against the file that holds server commands, env secrets, and reviewed-plugin settings.

Source

Thrown at crates/tui/src/mcp.rs:3571

        anyhow::anyhow!(
            "Failed to parse MCP config {}; file contents were omitted",
            codewhale_config::quote_os_path(path)
        )
    })
}

fn read_mcp_config_file(path: &Path) -> Result<Option<String>> {
    let metadata = match fs::symlink_metadata(path) {
        Ok(metadata) => metadata,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(err) => {
            return Err(err)
                .with_context(|| format!("Failed to inspect MCP config {}", path.display()));
        }
    };
    let file_type = metadata.file_type();
    if file_type.is_symlink() || !file_type.is_file() {
        anyhow::bail!("MCP config path must be a regular file: {}", path.display());
    }

    let mut file = open_mcp_config_file(path)
        .with_context(|| format!("Failed to read MCP config {}", path.display()))?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)
        .with_context(|| format!("Failed to read MCP config {}", path.display()))?;
    Ok(Some(contents))
}

#[cfg(unix)]
fn open_mcp_config_file(path: &Path) -> std::io::Result<fs::File> {
    use std::os::unix::fs::OpenOptionsExt;

    fs::OpenOptions::new()
        .read(true)
        .custom_flags(libc::O_NOFOLLOW)
        .open(path)

View on GitHub (pinned to 8880682c63)

Solutions

  1. Replace the symlink with the real file (copy the target into place) or use a hardlink/bind-mount instead
  2. If a dotfiles manager owns the file, switch it to copy/template mode rather than symlink
  3. Inspect with ls -l and remove any non-regular file occupying the path

Example fix

# before
~/.codewhale/mcp.json -> /home/me/dotfiles/mcp.json  (symlink)

# after
mv ~/.codewhale/mcp.json ~/.codewhale/mcp.json.bak
cp /home/me/dotfiles/mcp.json ~/.codewhale/mcp.json
Defensive patterns

Strategy: validation

Validate before calling

let meta = std::fs::symlink_metadata(&mcp_json_path)?;
let ft = meta.file_type();
if ft.is_symlink() || !ft.is_file() {
    anyhow::bail!("MCP config path must be a real file, not a symlink or special file");
}
codewhale_tui::mcp::load_config(&mcp_json_path)?;

Type guard

fn is_regular_config_file(path: &std::path::Path) -> bool {
    std::fs::symlink_metadata(path)
        .map(|m| !m.file_type().is_symlink() && m.file_type().is_file())
        .unwrap_or(false)
}

Try / catch

match load_config(&path) {
    Err(e) if e.to_string().contains("must be a regular file") => {
        // resolve the symlink target and copy it into place, then retry
    }
    other => other,
}

Prevention

When it happens

Trigger: Pointing the MCP config path at a symlink (e.g. ~/.codewhale/mcp.json linking into a dotfiles repo); the path being a directory or a named pipe.

Common situations: Dotfiles managers that symlink configs into place; provisioning scripts that accidentally create the path as a directory; container setups that expose the config through a symlink.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/4a56b3369da46fd5. Report an issue: GitHub.