{"record":{"id":"2246c3dd229b3c4f","repo":"jdx/mise","slug":"path-must-identify-an-output-beneath-the-task-dire","errorCode":null,"errorMessage":"path must identify an output beneath the task directory","messagePattern":"path must identify an output beneath the task directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/task/task_cache.rs","lineNumber":1264,"sourceCode":"    build_output_matcher(root, &patterns)\n        .wrap_err_with(|| format!(\"task {} has an invalid cache output pattern\", task.name))?;\n    Ok(())\n}\n\nfn ensure_safe_relative(path: &Path) -> Result<()> {\n    if path.as_os_str().is_empty() || path.is_absolute() {\n        bail!(\"path must be a non-empty relative path\");\n    }\n    if path.components().any(|c| {\n        matches!(\n            c,\n            Component::ParentDir | Component::RootDir | Component::Prefix(_)\n        )\n    }) {\n        bail!(\"path must not escape the task directory\");\n    }\n    if !path.components().any(|c| matches!(c, Component::Normal(_))) {\n        bail!(\"path must identify an output beneath the task directory\");\n    }\n    Ok(())\n}\n\nfn resolve_output_roots(task: &Task, root: &Path, require_matches: bool) -> Result<Vec<PathBuf>> {\n    let mut resolved = BTreeSet::new();\n    let patterns = task.outputs.patterns();\n    let matcher = build_output_matcher(root, &patterns)?;\n    for output in output_glob_patterns(&patterns) {\n        ensure_safe_relative(Path::new(&output))?;\n        if crate::task::task_source_checker::is_glob_pattern(&output) {\n            let mut glob_matched = false;\n            for expanded in expand_enumeration_patterns(&output)? {\n                ensure_safe_relative(Path::new(&expanded))?;\n                for entry in glob_walk(&root.join(expanded), false)? {\n                    let path = match entry {\n                        Ok(entry) => entry.into_path(),\n                        Err(err) => match symlink_walk_error_path(&err) {","sourceCodeStart":1246,"sourceCodeEnd":1282,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/task/task_cache.rs#L1246-L1282","documentation":"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.","triggerScenarios":"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.","commonSituations":"Misunderstanding that '.' means 'whole directory'; programmatic generation of output lists that emits empty strings; templated outputs that resolved to '.'.","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"],"exampleFix":"# before\noutputs = ['.']\n# after\noutputs = ['dist/']","handlingStrategy":"validation","validationCode":"fn identifies_output(p: &str) -> bool {\n    std::path::Path::new(p).components()\n        .any(|c| matches!(c, std::path::Component::Normal(_)))\n}","typeGuard":"fn concrete_output(p: &str) -> Option<&str> {\n    identifies_output(p).then_some(p)\n}","tryCatchPattern":null,"preventionTips":["Use concrete artifact paths/dirs, not '.' or ''","Filter empty strings from templated output lists"],"tags":["path","validation","task-cache"],"backgroundTag":"invalid-argument-value","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}