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
- Replace '.' with the concrete directory or file the task produces (e.g. `dist/`)
- Remove empty entries from the outputs list
- 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
- Use concrete artifact paths/dirs, not '.' or ''
- Filter empty strings from templated output lists
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
- path must not escape the task directory
- bootstrap_command returned an unsafe mise path
- brew-cask: command_wrapper requires a command name without…
- install_mise must be an absolute path or start with ~/
- install_mise must name an executable file
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)