{"record":{"id":"ffd69900cefe47a6","repo":"Hmbown/CodeWhale","slug":"reviewed-plugin-mcp-path-escaped-its-staged-root","errorCode":null,"errorMessage":"reviewed plugin MCP path escaped its staged root","messagePattern":"reviewed plugin MCP path escaped its staged root","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":3816,"sourceCode":"                anyhow::bail!(\"reviewed plugin MCP argument path escaped its staged root\");\n            }\n            *argument = frozen.display().to_string();\n        }\n    }\n    Ok(())\n}\n\nfn resolve_plugin_mcp_cwd(plugin_path: &Path, cwd: Option<&Path>) -> Result<PathBuf> {\n    let cwd = match cwd {\n        Some(cwd) if cwd.is_relative() => normalize_path_components(&plugin_path.join(cwd)),\n        Some(cwd) => normalize_path_components(cwd),\n        None => plugin_path.to_path_buf(),\n    };\n    let resolved = cwd\n        .canonicalize()\n        .unwrap_or_else(|_| normalize_path_components(&cwd));\n    if !resolved.starts_with(plugin_path) {\n        anyhow::bail!(\"reviewed plugin MCP path escaped its staged root\");\n    }\n    Ok(resolved)\n}\n\nfn workspace_allows_project_mcp_config(workspace: &Path) -> bool {\n    crate::config::is_workspace_trusted(workspace)\n}\n\nfn checked_workspace_mcp_config_path(workspace: &Path) -> Result<PathBuf> {\n    Ok(checked_workspace_path(workspace)?\n        .join(\".codewhale\")\n        .join(\"mcp.json\"))\n}\n\nfn checked_workspace_path(workspace: &Path) -> Result<PathBuf> {\n    if workspace.as_os_str().is_empty() {\n        anyhow::bail!(\"workspace path cannot be empty\");\n    }","sourceCodeStart":3798,"sourceCodeEnd":3834,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L3798-L3834","documentation":"resolve_plugin_mcp_cwd resolves a reviewed plugin MCP server's cwd (default: the plugin's staged root; relative cwds are joined onto it, absolute ones taken as-is), canonicalizes it, and requires the result to stay under the plugin's staged path. Otherwise the reviewed plugin would spawn its server process outside the reviewed sandbox, so the config is rejected.","triggerScenarios":"A plugin manifest sets cwd to an absolute path outside the staged root, a relative '../..' path, or the staged tree contains a symlink that canonicalization resolves outside the root.","commonSituations":"Porting a plugin that previously ran from the repo root; a plugin assuming its cwd is the user's project directory rather than the staged tree.","solutions":["Set cwd to '.' or a subdirectory of the plugin root in the plugin manifest","Drop absolute cwd values - reviewed plugins must run inside their staged tree","Re-stage the plugin after fixing the manifest and re-review it"],"exampleFix":"// before (plugin manifest)\n\"cwd\": \"/home/me/projects/my-plugin\"\n\n// after\n\"cwd\": \".\"","handlingStrategy":"validation","validationCode":"// Validate plugin cwd containment before registering the server:\nlet resolved = cwd_candidate.canonicalize().unwrap_or_else(|_| normalize_path_components(&cwd_candidate));\nanyhow::ensure!(resolved.starts_with(&plugin_staged_root), \"plugin cwd escapes staged root\");","typeGuard":"fn plugin_cwd_within_root(cwd: &std::path::Path, root: &std::path::Path) -> bool {\n    cwd.canonicalize()\n        .map(|c| c.starts_with(root))\n        .unwrap_or_else(|_| normalize_path_components(cwd).starts_with(root))\n}","tryCatchPattern":null,"preventionTips":["Default reviewed plugin servers to cwd '.' inside the staged tree","Never ship absolute cwd paths in plugin manifests","Test plugins from their staged location, not the source checkout, during review"],"tags":["mcp","plugins","security","cwd","path-traversal"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}