{"record":{"id":"f74f64caad83e30b","repo":"github/spec-kit","slug":"invalid-command-file-label-reason","errorCode":null,"errorMessage":"Invalid command 'file' {label}: {reason}","messagePattern":"Invalid command 'file' (.+?): (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":471,"sourceCode":"            # relative_extension_path_violation() below already rejects a\n            # non-string value.\n            if not isinstance(cmd[\"name\"], str):\n                raise ValidationError(\n                    f\"Invalid command name: expected a string, \"\n                    f\"got {type(cmd['name']).__name__}\"\n                )\n\n            # Validate the 'file' field at manifest-load time using the single\n            # shared policy in relative_extension_path_violation(), so manifest\n            # validation cannot drift from the runtime registrar guard. This is\n            # defense-in-depth: the command/skill/preset readers also contain\n            # the resolved path, but rejecting an unsafe value here surfaces a\n            # clear error instead of silently skipping the command.\n            cmd_file = cmd[\"file\"]\n            reason = relative_extension_path_violation(cmd_file)\n            if reason:\n                label = repr(cmd_file) if isinstance(cmd_file, str) else f\"for command '{cmd.get('name')}'\"\n                raise ValidationError(f\"Invalid command 'file' {label}: {reason}\")\n\n            # Validate command name format\n            if not EXTENSION_COMMAND_NAME_PATTERN.match(cmd[\"name\"]):\n                corrected = self._try_correct_command_name(cmd[\"name\"], ext[\"id\"])\n                if corrected:\n                    self.warnings.append(\n                        f\"Command name '{cmd['name']}' does not follow the required pattern \"\n                        f\"'speckit.{{extension}}.{{command}}'. Registering as '{corrected}'. \"\n                        f\"The extension author should update the manifest to use this name.\"\n                    )\n                    rename_map[cmd[\"name\"]] = corrected\n                    cmd[\"name\"] = corrected\n                else:\n                    raise ValidationError(\n                        f\"Invalid command name '{cmd['name']}': \"\n                        \"must follow pattern 'speckit.{extension}.{command}'\"\n                    )\n","sourceCodeStart":453,"sourceCodeEnd":489,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L453-L489","documentation":"The 'file' field of a provides.commands entry failed the shared path-safety policy in relative_extension_path_violation() (src/specify_cli/_utils.py:21). That single policy is reused by both manifest-load validation and the runtime CommandRegistrar guard so the two cannot drift. It rejects non-string/empty values, leading/trailing whitespace, backslash separators, absolute/anchored paths (POSIX /abs, Windows C:\\ or C:foo, UNC), '..' traversal, directory-suffixed values, and platform-reserved/invalid components. The label in the message is the repr of the offending string, or the command name when the value is not even a string.","triggerScenarios":"A command entry with \"file\": \"/etc/commands/x.md\", \"file\": \"..\\\\..\\\\x.md\", \"file\": \"commands/\" (directory suffix), \"file\": \" commands/x.md \" (whitespace), or a non-string file value such as null or 3. Any of these hit ExtensionManifest._validate() during extension install/load.","commonSituations":"Authors copying absolute paths from local dev machines into the manifest; Windows authors using backslashes; manifests written by hand with trailing slashes; thinking 'file' can point outside the extension directory. Because the policy is defense-in-depth, even values the registrar might have skipped now fail loudly at load time.","solutions":["Change the file value to a relative, forward-slash path inside the extension directory, e.g. \"commands/build.md\".","Remove leading/trailing whitespace and any '..' segments, drive letters, or leading slashes.","Ensure the value is a non-empty string (not null/int) and names a file, not a directory (no trailing '/')."],"exampleFix":"// before\n{ \"name\": \"speckit.myext.build\", \"file\": \"/home/me/ext/commands/build.md\" }\n// after\n{ \"name\": \"speckit.myext.build\", \"file\": \"commands/build.md\" }","handlingStrategy":"validation","validationCode":"from specify_cli._utils import relative_extension_path_violation\n\nfor cmd in manifest.get(\"provides\", {}).get(\"commands\", []):\n    reason = relative_extension_path_violation(cmd.get(\"file\"))\n    if reason:\n        raise SystemExit(f\"bad file for {cmd.get('name')}: {reason}\")","typeGuard":"def is_safe_relative_file(value: object) -> bool:\n    from specify_cli._utils import relative_extension_path_violation\n    return relative_extension_path_violation(value) is None","tryCatchPattern":"try:\n    ExtensionManifest.load(path)\nexcept ValidationError as e:\n    if \"Invalid command 'file'\" in str(e):\n        # rewrite file to a relative forward-slash path inside the extension dir\n        ...","preventionTips":["Never put absolute paths or '..' in a manifest file field.","Use forward slashes only; trim whitespace; no trailing '/'.","Run relative_extension_path_violation() as a pre-install lint step."],"tags":["extensions","manifest","path-traversal","validation","security"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}