github/spec-kit · error · ValidationError
Invalid hook '{hook_name}': expected a mapping or list of ma
Error message
Invalid hook '{hook_name}': expected a mapping or list of mappings What it means
Each entry produced by coerce_hook_entries(hook_config) must be a mapping. coerce_hook_entries normalizes a single mapping or a list into entries; after normalization anything that is not a dict (e.g. a bare string entry inside the list) is rejected.
Source
Thrown at src/specify_cli/extensions/__init__.py:420
raise ValidationError(
"Extension must provide at least one command, hook, or event "
"(or a declared template/script)"
)
self._validate_provided_artifacts(templates, section="templates", singular="template")
self._validate_provided_artifacts(scripts, section="scripts", singular="script")
# Validate hook values (if present).
# Each event is a single mapping or a list of mappings.
if hooks:
for hook_name, hook_config in hooks.items():
if isinstance(hook_config, list) and not hook_config:
raise ValidationError(
f"Invalid hook '{hook_name}': list must contain at least one entry"
)
for entry in coerce_hook_entries(hook_config):
if not isinstance(entry, dict):
raise ValidationError(
f"Invalid hook '{hook_name}': "
"expected a mapping or list of mappings"
)
if not entry.get("command"):
raise ValidationError(
f"Hook '{hook_name}' missing required 'command' field"
)
if "priority" in entry:
priority = entry["priority"]
if not isinstance(priority, int) or isinstance(priority, bool):
raise ValidationError(
f"Hook '{hook_name}' has invalid 'priority': "
"must be an integer"
)
if priority < 1:
raise ValidationError(
f"Hook '{hook_name}' has invalid 'priority': "
"must be >= 1"View on GitHub (pinned to bf88c9f9a8)
Solutions
- Make every entry a mapping: `- command: ./setup.sh`.
- Add other supported keys (e.g. priority) inside the mapping, not as siblings.
- A single mapping (no list) is also fine — the normalizer wraps it.
Example fix
# before
hooks:
post-install:
- ./setup.sh
# after
hooks:
post-install:
- command: ./setup.sh Defensive patterns
Strategy: type-guard
Validate before calling
def hook_entries_all_mappings(data: dict) -> bool:
hooks = data.get("hooks") or {}
for cfg in hooks.values():
entries = cfg if isinstance(cfg, list) else [cfg]
if not all(isinstance(e, dict) for e in entries):
return False
return True Type guard
def is_hook_entry(v: object) -> bool:
return isinstance(v, dict) Prevention
- Every hook entry is a mapping with a command key — no bare string shorthand.
- A single mapping is fine; the loader normalizes it to a one-entry list.
When it happens
Trigger: `hooks:\n post-install:\n - ./setup.sh` — the list element is a string, not a mapping with a command key. The isinstance(entry, dict) check fails.
Common situations: Author writes hook entries as bare command strings, assuming the schema accepts a string shorthand, or mixes a string into an otherwise valid list of mappings.
Related errors
- Invalid hooks: expected a mapping
- Invalid hook '{hook_name}': list must contain at least one e
- Hook '{hook_name}' missing required 'command' field
- Hook '{hook_name}' has invalid 'priority': must be an intege
- Invalid extension ID '{ext['id']}': must be lowercase alphan
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/464d2d0eba41c4b7.
Report an issue: GitHub.