jdx/mise · error

path must not escape the task directory

Error message

path must not escape the task directory

What it means

mise validates task cache output paths with ensure_safe_relative, which rejects any relative path containing ParentDir (..), RootDir (/), or Windows Prefix components. This prevents a task's cache outputs from referencing or overwriting files outside the task's working directory. It is a deliberate path-traversal guard applied to each `outputs` entry in a task definition.

Solutions

  1. Make the output path relative and inside the task directory (e.g. `dist/` instead of `../dist`)
  2. If outputs genuinely live outside the task dir, relocate them into the task dir or change the task's dir
  3. Remove leading `/` or drive prefixes and any `..` components from the pattern

Example fix

# before
[task.build]
outputs = ['../shared/dist']
# after
[task.build]
outputs = ['dist']
Defensive patterns

Strategy: validation

Validate before calling

fn is_safe_output(p: &str) -> bool {
    let path = std::path::Path::new(p);
    !path.is_empty() && !path.is_absolute()
        && !path.components().any(|c| matches!(c,
            std::path::Component::ParentDir
            | std::path::Component::RootDir
            | std::path::Component::Prefix(_)))
}

Type guard

fn safe_output(p: &str) -> Option<&str> {
    is_safe_output(p).then_some(p)
}

Prevention

When it happens

Trigger: Declaring a task output like `outputs = ['../build']`, an absolute path like `/tmp/out`, or a Windows drive path (`C:\out`) in a task with cache outputs; the check runs when validating/resolving task cache outputs.

Common situations: Authors moving outputs to a shared directory outside the task dir; copying an absolute path from a build log; Windows users using drive-letter paths; typos like `..foo/..`.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/641245a542f670b1. Report an issue: GitHub.

Appendix: source

Thrown at src/task/task_cache.rs:1261

            )
        })?;
    }
    build_output_matcher(root, &patterns)
        .wrap_err_with(|| format!("task {} has an invalid cache output pattern", task.name))?;
    Ok(())
}

fn ensure_safe_relative(path: &Path) -> Result<()> {
    if path.as_os_str().is_empty() || path.is_absolute() {
        bail!("path must be a non-empty relative path");
    }
    if path.components().any(|c| {
        matches!(
            c,
            Component::ParentDir | Component::RootDir | Component::Prefix(_)
        )
    }) {
        bail!("path must not escape the task directory");
    }
    if !path.components().any(|c| matches!(c, Component::Normal(_))) {
        bail!("path must identify an output beneath the task directory");
    }
    Ok(())
}

fn resolve_output_roots(task: &Task, root: &Path, require_matches: bool) -> Result<Vec<PathBuf>> {
    let mut resolved = BTreeSet::new();
    let patterns = task.outputs.patterns();
    let matcher = build_output_matcher(root, &patterns)?;
    for output in output_glob_patterns(&patterns) {
        ensure_safe_relative(Path::new(&output))?;
        if crate::task::task_source_checker::is_glob_pattern(&output) {
            let mut glob_matched = false;
            for expanded in expand_enumeration_patterns(&output)? {
                ensure_safe_relative(Path::new(&expanded))?;
                for entry in glob_walk(&root.join(expanded), false)? {

View on GitHub (pinned to 533346cc37)