Hmbown/CodeWhale · error

Project MCP server cwd must stay within workspace: {}

Error message

Project MCP server cwd must stay within workspace: {}

What it means

Servers declared in a workspace's .codewhale/mcp.json get their cwd resolved like plugin cwds: relative cwds are joined to the workspace, the result is canonicalized, and it must remain inside the (canonicalized) workspace root. A cwd that escapes via absolute path, '..', or a symlink is rejected so a project config cannot launch processes outside the project.

Source

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

    } else {
        std::env::current_dir()
            .unwrap_or_else(|_| PathBuf::from("."))
            .join(workspace)
    };
    normalize_path_components(&absolute)
}

fn resolve_project_mcp_cwd(workspace: &Path, cwd: Option<&Path>) -> Result<PathBuf> {
    let cwd = match cwd {
        Some(cwd) if cwd.is_relative() => normalize_path_components(&workspace.join(cwd)),
        Some(cwd) => normalize_path_components(cwd),
        None => workspace.to_path_buf(),
    };
    let resolved = cwd
        .canonicalize()
        .unwrap_or_else(|_| normalize_path_components(&cwd));
    if !resolved.starts_with(workspace) {
        anyhow::bail!(
            "Project MCP server cwd must stay within workspace: {}",
            resolved.display()
        );
    }
    Ok(resolved)
}

fn normalize_path_components(path: &Path) -> PathBuf {
    let mut normalized = PathBuf::new();
    for component in path.components() {
        match component {
            Component::Prefix(_) | Component::RootDir => {
                normalized.push(component.as_os_str());
            }
            Component::CurDir => {}
            Component::ParentDir => {
                normalized.pop();
            }

View on GitHub (pinned to 8880682c63)

Solutions

  1. Keep the server process cwd inside the workspace ('.' or a subdirectory)
  2. If the server must run elsewhere, declare it in the user-level ~/.codewhale/mcp.json instead of the project config
  3. Remove symlink hops inside the workspace so canonicalized paths stay under the workspace root

Example fix

// before (.codewhale/mcp.json)
"build-server": { "command": "./watch.sh", "cwd": "../shared-scripts" }

// after
"build-server": { "command": "./watch.sh", "cwd": "." }
Defensive patterns

Strategy: validation

Validate before calling

// Before starting a project MCP server, verify its cwd stays in the workspace:
let resolved = declared_cwd.canonicalize().unwrap_or_else(|_| normalize_path_components(&declared_cwd));
anyhow::ensure!(resolved.starts_with(&canonical_workspace), "project MCP cwd escapes workspace");

Type guard

fn project_cwd_within_workspace(cwd: &std::path::Path, workspace: &std::path::Path) -> bool {
    cwd.canonicalize()
        .map(|c| c.starts_with(workspace))
        .unwrap_or_else(|_| normalize_path_components(cwd).starts_with(workspace))
}

Prevention

When it happens

Trigger: A project .codewhale/mcp.json server entry with cwd set to an absolute path outside the workspace, a relative path containing '..', or a directory that is a symlink pointing outside the workspace.

Common situations: A repo whose MCP server expects to run from a monorepo sibling directory; the workspace path itself containing a symlink so canonicalization changes the prefix; moving the repo so a previously-inner path resolves elsewhere.

Related errors


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