{"record":{"id":"9470530e65ef8255","repo":"jdx/mise","slug":"workspace-project-id-has-root-root-that-es","errorCode":null,"errorMessage":"workspace project {id:?} has root {root:?} that escapes the workspace root","messagePattern":"workspace project (.+?) has root (.+?) that escapes the workspace root","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/task/workspace.rs","lineNumber":1202,"sourceCode":"        })\n        .collect()\n}\n\nfn normalize_project_root(id: &ProjectId, root: &Path) -> Result<PathBuf> {\n    if root.is_absolute() {\n        bail!(\n            \"workspace project {id:?} has absolute root {root:?}; roots must be workspace-relative\"\n        );\n    }\n\n    let mut normalized = PathBuf::new();\n    for component in root.components() {\n        match component {\n            Component::CurDir => {}\n            Component::Normal(component) => normalized.push(component),\n            Component::ParentDir => {\n                if !normalized.pop() {\n                    bail!(\n                        \"workspace project {id:?} has root {root:?} that escapes the workspace root\"\n                    );\n                }\n            }\n            Component::RootDir | Component::Prefix(_) => {\n                bail!(\n                    \"workspace project {id:?} has absolute root {root:?}; roots must be workspace-relative\"\n                );\n            }\n        }\n    }\n    if normalized.as_os_str().is_empty() {\n        normalized.push(\".\");\n    }\n    Ok(normalized)\n}\n\nfn normalize_workspace_path(path: &Path) -> Result<PathBuf> {","sourceCodeStart":1184,"sourceCodeEnd":1220,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/task/workspace.rs#L1184-L1220","documentation":"normalize_project_root() walks the components of a relative project root; each '..' must be absorbed by a preceding normal component. If a '..' pops past the accumulated path (normalized.pop() returns false), the root escapes the workspace root and is rejected — e.g. root = \"../sibling\" or \"crates/../../outside\".","triggerScenarios":"A [monorepo.projects] override root containing '..' segments that resolve above the workspace root; a provider trying to attach a project living outside the monorepo directory; workspace root moved deeper (directory restructure) so previously valid '../x' paths now escape.","commonSituations":"Monorepo root relocated one level (repo/repo instead of repo) breaking existing overrides; attempting to include a sibling checkout as a workspace project; copy-pasted relative paths from a different layout.","solutions":["Point the root at a directory inside the workspace root (no escaping '..' chain)","If the project genuinely lives outside, move it inside or move the workspace root up so the target is contained","Replace '../..' chains with the direct in-root relative path"],"exampleFix":"# before (mise.toml)\n[monorepo.projects.\"cargo:shared\"]\nroot = \"../shared-monorepo/crates/shared\"\n\n# after — vendored inside the workspace\n[monorepo.projects.\"cargo:shared\"]\nroot = \"vendor/shared\"","handlingStrategy":"validation","validationCode":"fn stays_in_root(rel: &Path) -> bool {\n    let mut depth = 0usize;\n    for c in rel.components() {\n        match c {\n            std::path::Component::Normal(_) => depth += 1,\n            std::path::Component::ParentDir => { if depth == 0 { return false; } depth -= 1; }\n            _ => {}\n        }\n    }\n    true\n}\nassert!(stays_in_root(Path::new(&cfg.root.unwrap_or_default())));","typeGuard":"fn root_stays_in_workspace(root: &Path) -> bool {\n    let mut depth = 0;\n    root.components().all(|c| match c {\n        std::path::Component::Normal(_) => { depth += 1; true }\n        std::path::Component::ParentDir => { let ok = depth > 0; depth = depth.saturating_sub(1); ok }\n        _ => true,\n    })\n}","tryCatchPattern":null,"preventionTips":["Keep every [monorepo.projects] root inside the monorepo checkout","When the repo layout changes, grep overrides for '../' and re-baseline them","If a project must live outside, relocate the workspace root rather than escaping with '..'"],"tags":["rust","mise","workspace","path-validation","path-traversal"],"backgroundTag":"path-escapes-root","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}