{"record":{"id":"5736c77224679a80","repo":"github/spec-kit","slug":"hook-hook-name-must-be-a-mapping-or-list-of-ma","errorCode":null,"errorMessage":"Hook '{hook_name}' must be a mapping or list of mappings, got {type(entry).__name__}","messagePattern":"Hook '(.+?)' must be a mapping or list of mappings, got (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":519,"sourceCode":"            for alias in aliases:\n                if not isinstance(alias, str):\n                    raise ValidationError(\n                        f\"Aliases for command '{cmd['name']}' must be strings\"\n                    )\n                alias_reason = relative_extension_path_violation(alias)\n                if alias_reason:\n                    raise ValidationError(\n                        f\"Invalid alias {alias!r} for command \"\n                        f\"'{cmd['name']}': {alias_reason}\"\n                    )\n\n        # Rewrite any hook command references that pointed at a renamed command or\n        # an alias-form ref (ext.cmd → speckit.ext.cmd).  Always emit a warning when\n        # the reference is changed so extension authors know to update the manifest.\n        for hook_name, hook_data in self.data.get(\"hooks\", {}).items():\n            for entry in coerce_hook_entries(hook_data):\n                if not isinstance(entry, dict):\n                    raise ValidationError(\n                        f\"Hook '{hook_name}' must be a mapping or list of mappings, \"\n                        f\"got {type(entry).__name__}\"\n                    )\n                command_ref = entry.get(\"command\")\n                if not isinstance(command_ref, str):\n                    continue\n                # Step 1: apply any rename from the auto-correction pass.\n                after_rename = rename_map.get(command_ref, command_ref)\n                # Step 2: lift alias-form '{ext_id}.cmd' to canonical 'speckit.{ext_id}.cmd'.\n                parts = after_rename.split(\".\")\n                if len(parts) == 2 and parts[0] == ext[\"id\"]:\n                    final_ref = f\"speckit.{ext['id']}.{parts[1]}\"\n                else:\n                    final_ref = after_rename\n                if final_ref != command_ref:\n                    entry[\"command\"] = final_ref\n                    self.warnings.append(\n                        f\"Hook '{hook_name}' referenced command '{command_ref}'; \"","sourceCodeStart":501,"sourceCodeEnd":537,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L501-L537","documentation":"While rewriting hook command references, the loader found a hooks.<name> entry (after coerce_hook_entries() normalization) that is not a mapping. Each hook entry must be a dict so the rewriter can read its 'command' key; scalars, lists-of-scalars, or nested lists produce this ValidationError. The hook pass exists to rename refs pointed at auto-corrected command names and to lift alias-form '{ext_id}.cmd' refs to canonical 'speckit.{ext_id}.cmd'.","triggerScenarios":"\"hooks\": {\"post_install\": \"run.sh\"} (bare string) or a hook list containing a scalar like [\"run.sh\", {\"command\": \"...\"}]. Raised while iterating coerce_hook_entries() output in ExtensionManifest._validate().","commonSituations":"Author shorthand: writing a hook as a plain command string instead of an object; copy-pasting hook YAML from older extension examples with a different schema; malformed nesting after YAML edit.","solutions":["Wrap each hook entry as a mapping: \"hooks\": {\"post_install\": {\"command\": \"speckit.myext.setup\"}}.","If a hook holds multiple entries, use a list of mappings only — no bare strings inside.","Verify the hooks section against a known-good bundled extension (e.g. extensions/agent-context) after editing."],"exampleFix":"// before\n\"hooks\": { \"post_install\": \"speckit.myext.setup\" }\n// after\n\"hooks\": { \"post_install\": { \"command\": \"speckit.myext.setup\" } }","handlingStrategy":"validation","validationCode":"for hook_name, hook_data in manifest.get(\"hooks\", {}).items():\n    entries = hook_data if isinstance(hook_data, list) else [hook_data]\n    for e in entries:\n        if not isinstance(e, dict):\n            raise SystemExit(f\"hook '{hook_name}' entry is not a mapping: {e!r}\")","typeGuard":"def hooks_are_mappings(manifest: dict) -> bool:\n    for hook in manifest.get(\"hooks\", {}).values():\n        for e in (hook if isinstance(hook, list) else [hook]):\n            if not isinstance(e, dict):\n                return False\n    return True","tryCatchPattern":"try:\n    ExtensionManifest.load(path)\nexcept ValidationError as e:\n    if \"must be a mapping or list of mappings\" in str(e):\n        # wrap the hook value as {\"command\": ...} and retry\n        ...","preventionTips":["Always author hooks as objects with a 'command' key.","Use lists only for multiple mapping entries.","Validate the hooks block against a bundled extension example."],"tags":["extensions","manifest","hooks","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}