{"record":{"id":"ef0ffbc389f63dfa","repo":"affaan-m/ECC","slug":"what-must-match-output-component-pattern-to-name-an-output","errorCode":null,"errorMessage":"{what} must match {_OUTPUT_COMPONENT.pattern} to name an output file; pass --out","messagePattern":"(.+?) must match (.+?) to name an output file; pass --out","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/cli.py","lineNumber":80,"sourceCode":"    profile = interview_mod.conduct(answers, genre=args.genre)\n    out = Path(args.out) if args.out else Path(f\"{args.genre}-profile.json\")\n    out.write_text(json.dumps(profile, indent=2), encoding=\"utf-8\")\n    print(out)\n    return EXIT_OK\n\n\n_OUTPUT_COMPONENT = re.compile(r\"^[a-z0-9][a-z0-9_-]*$\")\n\n\ndef _output_component(value: Any, what: str) -> str:\n    \"\"\"Return ``value`` only when it is safe to use as an output filename part.\n\n    Pack names and genres come from operator-authored JSON. They are only\n    used to derive default output paths, so they must never carry path\n    separators or traversal; the same pattern the manifest schema declares.\n    \"\"\"\n    if not isinstance(value, str) or not _OUTPUT_COMPONENT.fullmatch(value):\n        raise ValueError(\n            f\"{what} must match {_OUTPUT_COMPONENT.pattern} to name an output file; pass --out\"\n        )\n    return value\n\n\ndef cmd_distill(args: argparse.Namespace) -> int:\n    if args.live:\n        print(distill_mod._FAIL_CLOSED, file=sys.stderr)\n        return EXIT_FAIL_CLOSED\n    profile = json.loads(Path(args.profile).read_text(encoding=\"utf-8\"))\n    sp = pack_mod.load(args.pack) if args.pack else None\n    spec = distill_mod.distill_local(profile, sp)\n    out = (Path(args.out) if args.out\n           else Path(f\"{_output_component(profile.get('genre', 'spec'), 'profile genre')}-spec.json\"))\n    out.write_text(json.dumps(spec, indent=2), encoding=\"utf-8\")\n    print(out)\n    return EXIT_OK\n","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/cli.py#L62-L98","documentation":"_output_component derives a default output filename from operator-authored pack names or genres. Because the value becomes a path component, it must be a plain string matching the manifest schema's safe-component pattern (_OUTPUT_COMPONENT) — no separators or traversal. The library throws this ValueError to block path traversal via untrusted JSON fields.","triggerScenarios":"Running `tasteforge distill` or `tasteforge apply` where the pack name/genre string is non-str (e.g. parsed as int), empty, or contains characters outside the allowed pattern such as '/', '\\\\', '..', or spaces.","commonSituations":"Pack JSON authored with a name like '../exfil' or 'sub/dir/pack', names containing spaces or unicode punctuation, or YAML/JSON where the field parses to a number instead of a string.","solutions":["Pass an explicit safe output path via the --out CLI flag to bypass name-derived defaults.","Fix the pack name/genre in the operator-authored JSON to match the allowed pattern (single path component, no separators).","Inspect _OUTPUT_COMPONENT in cli.py and conform the value to exactly that regex.","Quote/escape shell input when generating JSON so values don't pick up slashes or whitespace."],"exampleFix":"// before\n{name: \"../packs/my pack\"}\ntasteforge distill pack.json\n// after\n{name: \"my-pack\"}\ntasteforge distill pack.json   # or: tasteforge distill pack.json --out out/my-pack.tz","handlingStrategy":"validation","validationCode":"import re\n_OUTPUT_COMPONENT = re.compile(r'[A-Za-z0-9._-]+')\nassert isinstance(name, str) and _OUTPUT_COMPONENT.fullmatch(name), f\"pass --out for {name!r}\"","typeGuard":"def is_safe_component(value) -> bool:\n    return isinstance(value, str) and bool(_OUTPUT_COMPONENT.fullmatch(value))","tryCatchPattern":"try:\n    run_distill(args)\nexcept ValueError as e:\n    if 'must match' in str(e) and 'pass --out' in str(e):\n        sys.exit(f\"Invalid name in pack JSON: {e}. Re-run with --out <path>.\")\n    raise","preventionTips":["Pass --out explicitly whenever pack names are not fully trusted","Keep pack/genre names limited to [A-Za-z0-9._-]","Never allow path separators or '..' in name-derived output fields","Add a pre-flight regex check on names when generating pack JSON"],"tags":["python","cli","path-traversal","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}