jdx/mise · error

path must not escape the task directory

Error message

path must not escape the task directory

What it means

ensure_safe_relative rejects output paths containing '..', a root component, or a Windows prefix. Such paths can resolve outside the task directory when the archive is packed or restored, so mise refuses them outright as a path-traversal guard.

Source

Thrown at src/task/task_cache.rs:1262

            )
        })?;
    }
    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(root.join(expanded).to_str().unwrap_or_default())? {

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Point outputs at a path inside the project root, e.g. outputs = ['dist/']
  2. Produce artifacts inside the root and add a separate copy task for consumers elsewhere
  3. Remove '..' segments even where the filesystem would tolerate them

Example fix

# before
[tasks.build]
outputs = ['../shared/dist/app']

# after
[tasks.build]
outputs = ['dist/app']
Defensive patterns

Strategy: validation

Validate before calling

# CI / pre-commit: outputs never traverse upward
python3 - <<'PY'
import tomllib, pathlib
cfg = tomllib.loads(pathlib.Path('mise.toml').read_text())
for name, t in cfg.get('tasks', {}).items():
    for o in t.get('outputs', []):
        body = o.lstrip('!')
        assert '..' not in pathlib.PurePosixPath(body).parts, f'{name}: output escapes root: {o}'
PY

Prevention

When it happens

Trigger: An outputs pattern resolves to a path whose components include ParentDir/RootDir/Prefix — e.g. outputs = ['../shared/dist'] or a glob that expands through a path containing '..'.

Common situations: Monorepos where outputs land in a sibling package's directory; escaping the project to a shared build dir; '../' segments left in a copied pattern.

Related errors


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