jdx/mise · error

path must identify an output beneath the task directory

Error message

path must identify an output beneath the task directory

What it means

After rejecting traversal components, ensure_safe_relative requires at least one Normal component; a path that is empty, '.', or only separators identifies nothing beneath the task directory. mise throws this so cache outputs are real, addressable artifacts rather than the directory itself.

Solutions

  1. Replace '.' with the concrete directory or file the task produces (e.g. `dist/`)
  2. Remove empty entries from the outputs list
  3. If the whole task dir is the output, list the specific artifact directories instead

Example fix

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

Strategy: validation

Validate before calling

fn identifies_output(p: &str) -> bool {
    std::path::Path::new(p).components()
        .any(|c| matches!(c, std::path::Component::Normal(_)))
}

Type guard

fn concrete_output(p: &str) -> Option<&str> {
    identifies_output(p).then_some(p)
}

Prevention

When it happens

Trigger: Declaring `outputs = ['.']` or an empty/near-empty string as a task cache output; the check runs during output validation in resolve_output_roots/validate_outputs.

Common situations: Misunderstanding that '.' means 'whole directory'; programmatic generation of output lists that emits empty strings; templated outputs that resolved to '.'.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at src/task/task_cache.rs:1264

    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)? {
                    let path = match entry {
                        Ok(entry) => entry.into_path(),
                        Err(err) => match symlink_walk_error_path(&err) {

View on GitHub (pinned to 533346cc37)