{"record":{"id":"4f798833ab7614c7","repo":"github/spec-kit","slug":"invalid-singular-file-label-reason","errorCode":null,"errorMessage":"Invalid {singular} 'file' {label}: {reason}","messagePattern":"Invalid (.+?) 'file' (.+?): (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":615,"sourceCode":"                raise ValidationError(\n                    f\"Invalid {singular} name: expected a string, got {type(name).__name__}\"\n                )\n            if not VALID_EXTENSION_ARTIFACT_NAME_PATTERN.match(name):\n                raise ValidationError(\n                    f\"Invalid {singular} name '{name}': \"\n                    \"must be lowercase alphanumeric with hyphens only\"\n                )\n            if name in seen_names:\n                raise ValidationError(\n                    f\"Duplicate {singular} name '{name}' in 'provides.{section}'\"\n                )\n            seen_names.add(name)\n\n            file_value = entry[\"file\"]\n            reason = relative_extension_path_violation(file_value)\n            if reason:\n                label = repr(file_value) if isinstance(file_value, str) else f\"for {singular} '{name}'\"\n                raise ValidationError(f\"Invalid {singular} 'file' {label}: {reason}\")\n\n            if \"description\" in entry and not isinstance(entry[\"description\"], str):\n                raise ValidationError(\n                    f\"Invalid {singular} description for '{name}': expected a string\"\n                )\n\n            if \"strategy\" in entry:\n                raise ValidationError(\n                    f\"Invalid {singular} entry '{name}': 'strategy' is not authorable for \"\n                    \"extension-provided artifacts, which always use 'replace' semantics\"\n                )\n\n            if section == \"scripts\" and \"runtimes\" in entry:\n                runtimes = entry[\"runtimes\"]\n                if not isinstance(runtimes, list) or not all(\n                    isinstance(r, str) for r in runtimes\n                ):\n                    raise ValidationError(","sourceCodeStart":597,"sourceCodeEnd":633,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L597-L633","documentation":"The 'file' value of a provides.templates/scripts entry failed the shared path-safety policy in relative_extension_path_violation() (src/specify_cli/_utils.py:21) — the identical policy applied to command files, keeping manifest validation and the runtime registrar guard from drifting. It rejects non-string/empty values, whitespace padding, backslashes, absolute/anchored paths (POSIX, Windows drive/UNC), '..' traversal, trailing directory slashes, and platform-reserved components. The message label is repr(file) for strings, otherwise the artifact name.","triggerScenarios":"{\"file\": \"/opt/ext/t.md\"}, {\"file\": \"../shared/t.md\"}, {\"file\": \"templates\\\\t.md\"}, {\"file\": \"templates/\"}, or a non-string file value. Raised by the artifact validator right after the duplicate-name check.","commonSituations":"Absolute paths pasted from a dev machine; Windows separators; referencing a shared directory outside the extension with '../'; template generators emitting directory paths.","solutions":["Use a relative, forward-slash path inside the extension directory: \"templates/plan.md\".","Remove '..', leading '/', drive letters, and trailing slashes; trim whitespace.","Ship any shared file inside the extension directory instead of pointing outside it."],"exampleFix":"// before\n{ \"name\": \"plan\", \"file\": \"../shared/templates/plan.md\" }\n// after\n{ \"name\": \"plan\", \"file\": \"templates/plan.md\" }","handlingStrategy":"validation","validationCode":"from specify_cli._utils import relative_extension_path_violation\n\nfor section in (\"templates\", \"scripts\"):\n    for e in manifest.get(\"provides\", {}).get(section, []):\n        if isinstance(e, dict):\n            reason = relative_extension_path_violation(e.get(\"file\"))\n            assert reason is None, f\"unsafe {section} file {e.get('file')!r}: {reason}\"","typeGuard":"def is_safe_artifact_file(entry: dict) -> bool:\n    from specify_cli._utils import relative_extension_path_violation\n    return relative_extension_path_violation(entry.get(\"file\")) is None","tryCatchPattern":"try:\n    ExtensionManifest.load(path)\nexcept ValidationError as e:\n    if \"'file'\" in str(e) and \"must be\" in str(e):\n        # rewrite the path relative to the extension dir with forward slashes\n        ...","preventionTips":["All extension files must live inside the extension directory.","No absolute paths, '..', backslashes, or trailing '/'.","Lint with relative_extension_path_violation() pre-install."],"tags":["extensions","manifest","path-traversal","security","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}