{"record":{"id":"355da9298205473a","repo":"github/spec-kit","slug":"invalid-hooks-expected-a-mapping","errorCode":null,"errorMessage":"Invalid hooks: expected a mapping","messagePattern":"Invalid hooks: expected a mapping","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":390,"sourceCode":"        provides = self.data[\"provides\"]\n        if not isinstance(provides, dict):\n            raise ValidationError(\n                f\"Invalid provides: expected a mapping, got {type(provides).__name__}\"\n            )\n        commands = provides.get(\"commands\", [])\n        templates = provides.get(\"templates\", [])\n        scripts = provides.get(\"scripts\", [])\n        hooks = self.data.get(\"hooks\")\n        events = self.data.get(\"events\")\n\n        if \"commands\" in provides and not isinstance(commands, list):\n            raise ValidationError(\"Invalid provides.commands: expected a list\")\n        if \"templates\" in provides and not isinstance(templates, list):\n            raise ValidationError(\"Invalid provides.templates: expected a list\")\n        if \"scripts\" in provides and not isinstance(scripts, list):\n            raise ValidationError(\"Invalid provides.scripts: expected a list\")\n        if \"hooks\" in self.data and not isinstance(hooks, dict):\n            raise ValidationError(\"Invalid hooks: expected a mapping\")\n        if \"events\" in self.data:\n            from ..events import validate_events\n            validate_events(self.data)\n\n        has_commands = bool(commands)\n        has_hooks = bool(hooks)\n        has_events = bool(events)\n        has_templates = bool(templates)\n        has_scripts = bool(scripts)\n\n        if not has_commands and not has_hooks and not has_events and not has_templates and not has_scripts:\n            raise ValidationError(\n                \"Extension must provide at least one command, hook, or event \"\n                \"(or a declared template/script)\"\n            )\n\n        self._validate_provided_artifacts(templates, section=\"templates\", singular=\"template\")\n        self._validate_provided_artifacts(scripts, section=\"scripts\", singular=\"script\")","sourceCodeStart":372,"sourceCodeEnd":408,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L372-L408","documentation":"The optional top-level hooks section must be a mapping keyed by hook/event name, where each value is a single mapping or a list of mappings (normalized by coerce_hook_entries). A non-dict hooks value is rejected before per-hook validation.","triggerScenarios":"`hooks:` set to a list of `[event_name, config]` pairs, or a string. isinstance(hooks, dict) fails.","commonSituations":"Author writes hooks as a YAML list (one dash per hook) instead of a keyed mapping, which is the more common YAML shape for named entries.","solutions":["Rewrite hooks as a mapping: `hooks:\\n  post-install:\\n    command: ./script.sh`.","Remove list dashes at the hooks level; dashes belong only inside a hook's entry list.","Use a mapping or list-of-mappings for each hook value as per-hook validation expects."],"exampleFix":"# before\nhooks:\n  - post-install:\n      command: ./setup.sh\n\n# after\nhooks:\n  post-install:\n    command: ./setup.sh","handlingStrategy":"validation","validationCode":"def hooks_ok(data: dict) -> bool:\n    return \"hooks\" not in data or isinstance(data[\"hooks\"], dict)","typeGuard":"def is_hooks_mapping(data: dict) -> bool:\n    return \"hooks\" not in data or isinstance(data[\"hooks\"], dict)","tryCatchPattern":null,"preventionTips":["hooks is a name-keyed mapping; dashes belong inside a hook's entry list only.","Each value is a mapping or a list of mappings."],"tags":["extension-manifest","validation","hooks","yaml-structure"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}