{"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":1265,"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(root.join(expanded).to_str().unwrap_or_default())? {\n                    let path = entry?;\n                    glob_matched = true;\n                    let rel = path.strip_prefix(root)?.to_path_buf();","sourceCodeStart":1247,"sourceCodeEnd":1283,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/src/task/task_cache.rs#L1247-L1283","documentation":"The final ensure_safe_relative check: a valid cache path must contain at least one Component::Normal. Paths like \".\" or strings that normalize to no real component do not identify an output beneath the task directory, so they are rejected even though they are relative and non-escaping.","triggerScenarios":"outputs = [\".\"] (cache the whole cwd as a single 'root' of '.'), or an output pattern that reduces to a bare current-directory component; also fires for malformed manifest roots read at restore time.","commonSituations":"Users trying to cache 'everything the task touched' with outputs = [\".\"]; templating that collapses to '.'; edge-case manifests produced by other tools.","solutions":["Name concrete output paths or globs instead of '.'","Use outputs = [] plus explicit inclusion globs where possible","If seen during restore, delete the cache entry — manifests written by mise never contain such roots"],"exampleFix":"# before\noutputs = [\".\"]\n\n# after\noutputs = [\"dist/**\", \"build/**\"]","handlingStrategy":"validation","validationCode":"python3 - <<'EOF'\nimport tomllib, sys, os\ncfg = tomllib.load(open('mise.toml','rb'))\nfor name, t in cfg.get('tasks', {}).items():\n    for o in map(str, t.get('outputs', [])):\n        p = o.lstrip('\\\\').lstrip('!')\n        if os.path.normpath(p) in ('.', ''):\n            sys.exit(f'{name}: output must name a real path, not {o!r}')\nprint('ok')\nEOF","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use concrete paths/globs; '.' is not a valid cache root","Enumerate actual output dirs instead of caching the cwd"],"tags":["mise","task-cache","caching","paths","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T20:17:18.057Z"}