{"record":{"id":"1f9a74dd950e76a1","repo":"jdx/mise","slug":"workspace-path-path-escapes-the-workspace-root","errorCode":null,"errorMessage":"workspace path {path:?} escapes the workspace root","messagePattern":"workspace path (.+?) escapes the workspace root","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/task/workspace.rs","lineNumber":1232,"sourceCode":"    if normalized.as_os_str().is_empty() {\n        normalized.push(\".\");\n    }\n    Ok(normalized)\n}\n\nfn normalize_workspace_path(path: &Path) -> Result<PathBuf> {\n    if path.is_absolute() {\n        bail!(\"workspace path {path:?} is absolute; paths must be workspace-relative\");\n    }\n\n    let mut normalized = PathBuf::new();\n    for component in path.components() {\n        match component {\n            Component::CurDir => {}\n            Component::Normal(component) => normalized.push(component),\n            Component::ParentDir => {\n                if !normalized.pop() {\n                    bail!(\"workspace path {path:?} escapes the workspace root\");\n                }\n            }\n            Component::RootDir | Component::Prefix(_) => {\n                bail!(\"workspace path {path:?} is absolute; paths must be workspace-relative\");\n            }\n        }\n    }\n    if normalized.as_os_str().is_empty() {\n        normalized.push(\".\");\n    }\n    Ok(normalized)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[derive(Debug)]","sourceCodeStart":1214,"sourceCodeEnd":1250,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/task/workspace.rs#L1214-L1250","documentation":"While normalizing a workspace-relative path, normalize_workspace_path() rejects any '..' that pops past the path start: the path escapes the workspace root. This guards affected_projects_for_paths()/map_paths_to_projects() against out-of-tree paths like \"../other-repo/file\" that could otherwise never match a project root.","triggerScenarios":"Passing changed-path lists containing '../' entries: output of git diff run in a subdirectory without --relative, file-watcher events outside the root, or manually constructed diffs combining directory changes with '..'.","commonSituations":"Running git diff inside a project subdirectory (paths come out as ../../style); watching a parent directory that contains the workspace; joining relative paths incorrectly before calling the API.","solutions":["Strip the workspace-root prefix from absolute paths instead of building '../' relative paths","Run git with -C <workspace-root> --relative --name-only so diffs are root-relative","Filter out paths that strip_prefix(root) cannot resolve — they are outside the workspace"],"exampleFix":"# before\n mise$ git diff --name-only   # emits ../../crates/app/src/lib.rs\n\n# after\n git -C /repo diff --relative --name-only   # emits crates/app/src/lib.rs","handlingStrategy":"validation","validationCode":"fn within_root(root: &Path, rel: &Path) -> bool {\n    let mut depth = 0;\n    rel.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    }) && root.join(rel).starts_with(root)\n}\nlet safe: Vec<_> = paths.into_iter().filter(|p| within_root(root, p)).collect();","typeGuard":"fn path_does_not_escape(root: &Path, p: &Path) -> bool {\n    !p.is_absolute() && root.join(p).canonicalize().map(|c| c.starts_with(root)).unwrap_or(true)\n}","tryCatchPattern":null,"preventionTips":["Never hand-build '../' paths for change lists; derive them via strip_prefix","Generate diffs from the workspace root so output is already root-relative","Treat out-of-root change events as unrelated to workspace projects"],"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"}