jdx/mise · error

workspace project {id:?} has root {root:?} that escapes the

Error message

workspace project {id:?} has root {root:?} that escapes the workspace root

What it means

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".

Source

Thrown at src/task/workspace.rs:1202

        })
        .collect()
}

fn normalize_project_root(id: &ProjectId, root: &Path) -> Result<PathBuf> {
    if root.is_absolute() {
        bail!(
            "workspace project {id:?} has absolute root {root:?}; roots must be workspace-relative"
        );
    }

    let mut normalized = PathBuf::new();
    for component in root.components() {
        match component {
            Component::CurDir => {}
            Component::Normal(component) => normalized.push(component),
            Component::ParentDir => {
                if !normalized.pop() {
                    bail!(
                        "workspace project {id:?} has root {root:?} that escapes the workspace root"
                    );
                }
            }
            Component::RootDir | Component::Prefix(_) => {
                bail!(
                    "workspace project {id:?} has absolute root {root:?}; roots must be workspace-relative"
                );
            }
        }
    }
    if normalized.as_os_str().is_empty() {
        normalized.push(".");
    }
    Ok(normalized)
}

fn normalize_workspace_path(path: &Path) -> Result<PathBuf> {

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Point the root at a directory inside the workspace root (no escaping '..' chain)
  2. If the project genuinely lives outside, move it inside or move the workspace root up so the target is contained
  3. Replace '../..' chains with the direct in-root relative path

Example fix

# before (mise.toml)
[monorepo.projects."cargo:shared"]
root = "../shared-monorepo/crates/shared"

# after — vendored inside the workspace
[monorepo.projects."cargo:shared"]
root = "vendor/shared"
Defensive patterns

Strategy: validation

Validate before calling

fn stays_in_root(rel: &Path) -> bool {
    let mut depth = 0usize;
    for c in rel.components() {
        match c {
            std::path::Component::Normal(_) => depth += 1,
            std::path::Component::ParentDir => { if depth == 0 { return false; } depth -= 1; }
            _ => {}
        }
    }
    true
}
assert!(stays_in_root(Path::new(&cfg.root.unwrap_or_default())));

Type guard

fn root_stays_in_workspace(root: &Path) -> bool {
    let mut depth = 0;
    root.components().all(|c| match c {
        std::path::Component::Normal(_) => { depth += 1; true }
        std::path::Component::ParentDir => { let ok = depth > 0; depth = depth.saturating_sub(1); ok }
        _ => true,
    })
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17). Data as JSON: /api/errors/9470530e65ef8255. Report an issue: GitHub.