{"record":{"id":"aee778f038f7e443","repo":"github/spec-kit","slug":"invalid-extension-id-ext-id-must-be-lowerc","errorCode":null,"errorMessage":"Invalid extension ID '{ext['id']}': must be lowercase alphanumeric with hyphens only","messagePattern":"Invalid extension ID '(.+?)': must be lowercase alphanumeric with hyphens only","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":317,"sourceCode":"        # ``id: 2`` as an int -- and TypeError is not a ValidationError, so it\n        # escapes every caller that already handles a malformed manifest (see\n        # list_installed()'s \"Corrupted extension\" fallback, which catches\n        # ValidationError only, making one bad extension exit ``specify\n        # extension list`` with a raw traceback and hide the healthy ones).\n        # Mirrors the sibling IntegrationDescriptor, which already type-checks\n        # the same four fields.\n        for field in [\"id\", \"name\", \"version\", \"description\"]:\n            if field not in ext:\n                raise ValidationError(f\"Missing extension.{field}\")\n            if not isinstance(ext[field], str):\n                raise ValidationError(\n                    f\"Invalid extension.{field}: expected a string, \"\n                    f\"got {type(ext[field]).__name__}\"\n                )\n\n        # Validate extension ID format\n        if not re.match(r\"^[a-z0-9-]+$\", ext[\"id\"]):\n            raise ValidationError(\n                f\"Invalid extension ID '{ext['id']}': \"\n                \"must be lowercase alphanumeric with hyphens only\"\n            )\n\n        # Validate semantic version\n        try:\n            pkg_version.Version(ext[\"version\"])\n        except pkg_version.InvalidVersion:\n            raise ValidationError(f\"Invalid version: {ext['version']}\")\n\n        # Validate optional category field (free-form string)\n        if \"category\" in ext:\n            if not isinstance(ext[\"category\"], str) or not ext[\"category\"].strip():\n                raise ValidationError(\n                    \"Invalid extension.category: must be a non-empty string\"\n                )\n\n        # Validate optional effect field","sourceCodeStart":299,"sourceCodeEnd":335,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L299-L335","documentation":"Thrown by ExtensionManifest validation while parsing an extension's manifest (extension.yml). The extension.id field must match ^[a-z0-9-]+$ — lowercase letters, digits, and hyphens only. Any uppercase letter, underscore, space, or symbol in the ID fails the regex and aborts extension loading with a ValidationError.","triggerScenarios":"An extension manifest with `id: My_Extension`, `id: \"agent context\"`, or `id: ctx.v2` is loaded (via `specify extension add` or at init when bundled extensions install). re.match(r\"^[a-z0-9-]+$\", ext[\"id\"]) returns None and the error is raised.","commonSituations":"Authors naming an extension after a GitHub repo with dots (my-ext.repo), copy-pasting a display name into the id field, or converting underscores from a Python package name. Directory name and id mismatch also surfaces here.","solutions":["Set extension.id to a lowercase slug, e.g. `id: agent-context`.","Remove underscores, dots, spaces, and uppercase characters from the id value in extension.yml.","Rename the extension directory to match the corrected id so the registry stays consistent."],"exampleFix":"# before (extension.yml)\nid: My_Agent.Context\n\n# after\nid: my-agent-context","handlingStrategy":"validation","validationCode":"import re\n\ndef valid_extension_id(ext_id: object) -> bool:\n    return isinstance(ext_id, str) and re.fullmatch(r\"[a-z0-9-]+\", ext_id) is not None","typeGuard":"def is_valid_extension_id(v: object) -> bool:\n    return isinstance(v, str) and bool(re.fullmatch(r\"[a-z0-9-]+\", v))","tryCatchPattern":"try:\n    manifest = ExtensionManifest.load(path)\nexcept ValidationError as e:\n    print(f\"Manifest invalid: {e}\")","preventionTips":["Use a sluglified id (kebab-case) from the start; enforce it in CI with a manifest lint step.","Derive the id from the extension directory name, which is already constrained to the same charset."],"tags":["extension-manifest","validation","naming"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}