github/spec-kit · error · ValidationError
Hook '{hook_name}' must be a mapping or list of mappings, go
Error message
Hook '{hook_name}' must be a mapping or list of mappings, got {type(entry).__name__} What it means
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'.
Source
Thrown at src/specify_cli/extensions/__init__.py:519
for alias in aliases:
if not isinstance(alias, str):
raise ValidationError(
f"Aliases for command '{cmd['name']}' must be strings"
)
alias_reason = relative_extension_path_violation(alias)
if alias_reason:
raise ValidationError(
f"Invalid alias {alias!r} for command "
f"'{cmd['name']}': {alias_reason}"
)
# Rewrite any hook command references that pointed at a renamed command or
# an alias-form ref (ext.cmd → speckit.ext.cmd). Always emit a warning when
# the reference is changed so extension authors know to update the manifest.
for hook_name, hook_data in self.data.get("hooks", {}).items():
for entry in coerce_hook_entries(hook_data):
if not isinstance(entry, dict):
raise ValidationError(
f"Hook '{hook_name}' must be a mapping or list of mappings, "
f"got {type(entry).__name__}"
)
command_ref = entry.get("command")
if not isinstance(command_ref, str):
continue
# Step 1: apply any rename from the auto-correction pass.
after_rename = rename_map.get(command_ref, command_ref)
# Step 2: lift alias-form '{ext_id}.cmd' to canonical 'speckit.{ext_id}.cmd'.
parts = after_rename.split(".")
if len(parts) == 2 and parts[0] == ext["id"]:
final_ref = f"speckit.{ext['id']}.{parts[1]}"
else:
final_ref = after_rename
if final_ref != command_ref:
entry["command"] = final_ref
self.warnings.append(
f"Hook '{hook_name}' referenced command '{command_ref}'; "View on GitHub (pinned to bf88c9f9a8)
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.
Example fix
// before
"hooks": { "post_install": "speckit.myext.setup" }
// after
"hooks": { "post_install": { "command": "speckit.myext.setup" } } Defensive patterns
Strategy: validation
Validate before calling
for hook_name, hook_data in manifest.get("hooks", {}).items():
entries = hook_data if isinstance(hook_data, list) else [hook_data]
for e in entries:
if not isinstance(e, dict):
raise SystemExit(f"hook '{hook_name}' entry is not a mapping: {e!r}") Type guard
def hooks_are_mappings(manifest: dict) -> bool:
for hook in manifest.get("hooks", {}).values():
for e in (hook if isinstance(hook, list) else [hook]):
if not isinstance(e, dict):
return False
return True Try / catch
try:
ExtensionManifest.load(path)
except ValidationError as e:
if "must be a mapping or list of mappings" in str(e):
# wrap the hook value as {"command": ...} and retry
... Prevention
- 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.
When it happens
Trigger: "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().
Common situations: 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.
Related errors
- Manifest must be a YAML mapping, got {type(data).__name__}:
- Missing required field: {field}
- Invalid extension: expected a mapping, got {type(ext).__name
- Missing extension.{field}
- Invalid extension.{field}: expected a string, got {type(ext[
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/5736c77224679a80.
Report an issue: GitHub.