{"record":{"id":"79a574d99a8dac87","repo":"github/spec-kit","slug":"invalid-command-name-expected-a-string-got-type","errorCode":null,"errorMessage":"Invalid command name: expected a string, got {type(cmd['name']).__name__}","messagePattern":"Invalid command name: expected a string, got (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":456,"sourceCode":"                                \"must be >= 1\"\n                            )\n\n        # Validate commands; track renames so hook references can be rewritten.\n        rename_map: Dict[str, str] = {}\n        for cmd in commands:\n            if not isinstance(cmd, dict):\n                raise ValidationError(\n                    \"Each command entry in 'provides.commands' must be a mapping\"\n                )\n            if \"name\" not in cmd or \"file\" not in cmd:\n                raise ValidationError(\"Command missing 'name' or 'file'\")\n            # The pattern match below would raise a bare TypeError on a\n            # non-string name (``name: 2``), escaping the ValidationError\n            # contract. The 'file' field needs no check here:\n            # 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\"]):","sourceCodeStart":438,"sourceCodeEnd":474,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L438-L474","documentation":"Raised while validating an extension manifest's 'provides.commands' section: a command entry declared a 'name' key whose value is not a Python string (e.g. an int or bool). The explicit isinstance check exists because the later regex match (EXTENSION_COMMAND_NAME_PATTERN.match) would raise a bare TypeError on a non-string, escaping the ValidationError contract the loader relies on. It is a manifest-authoring error, caught at extension load/install time.","triggerScenarios":"An extension.json/manifest file contains something like \"commands\": [{\"name\": 2, \"file\": \"commands/foo.md\"}] or a YAML manifest where the name is unquoted and parses as a number/boolean (e.g. name: 42, name: true). Loading the manifest via ExtensionManifest validation during 'specify extension install' or registry add raises ValidationError.","commonSituations":"Hand-editing a manifest and forgetting quotes around a numeric-looking name; YAML parsing turning an unquoted value into a non-string; programmatic manifest generation that inserts an int/enum instead of str.","solutions":["Open the extension manifest and make every provides.commands[].name a quoted string, e.g. \"speckit.myext.build\".","If the manifest is generated by a script, coerce names with str(name) before serialization and re-run install.","Validate the manifest locally before install by json/yaml round-tripping and asserting isinstance(name, str) for each command entry."],"exampleFix":"// before (manifest.json)\n\"commands\": [{ \"name\": 2, \"file\": \"commands/build.md\" }]\n// after\n\"commands\": [{ \"name\": \"speckit.myext.build\", \"file\": \"commands/build.md\" }]","handlingStrategy":"validation","validationCode":"def valid_command_entries(provides: dict) -> list[str]:\n    errs = []\n    for c in provides.get(\"commands\", []):\n        if not isinstance(c, dict):\n            errs.append(\"non-mapping command entry\")\n        elif \"name\" in c and not isinstance(c[\"name\"], str):\n            errs.append(f\"command name is {type(c['name']).__name__}, expected str\")\n    return errs\n\nerrs = valid_command_entries(manifest.get(\"provides\", {}))\nassert not errs, errs","typeGuard":"def is_str_command_name(cmd: object) -> bool:\n    return isinstance(cmd, dict) and isinstance(cmd.get(\"name\"), str)","tryCatchPattern":"try:\n    ExtensionManifest.load(path)\nexcept ValidationError as e:\n    if \"Invalid command name: expected a string\" in str(e):\n        # fix the manifest's provides.commands[].name and retry\n        ...","preventionTips":["Always quote command names in JSON/YAML manifests.","If generating manifests, run a schema pass asserting name/file types before writing.","Keep a known-good bundled extension manifest as the template."],"tags":["manifest","validation","extensions","type-error"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}