github/spec-kit · error · ValidationError

Invalid event '{event_name}': expected a mapping

Error message

Invalid event '{event_name}': expected a mapping

What it means

Thrown by validate_events() when an entry under the manifest's `events` mapping is not itself a mapping. Each event name must map to a config dict (at minimum containing 'command'); a string, list, or null there fails this check.

Source

Thrown at src/specify_cli/events.py:1785

            failures.append((key, str(exc)))

    if failures:
        raise EventRefreshError(failures)


# -- Manifest validation ---------------------------------------------------

def validate_events(data: dict[str, Any]) -> None:
    """Validate ``events`` field in extension manifest data."""
    from .extensions import ValidationError

    events = data.get("events")
    if "events" in data and not isinstance(events, dict):
        raise ValidationError("Invalid events: expected a mapping")
    if events:
        for event_name, event_config in events.items():
            if not isinstance(event_config, dict):
                raise ValidationError(
                    f"Invalid event '{event_name}': expected a mapping"
                )
            command = event_config.get("command")
            # #17: command must be a non-empty string. A truthy non-string
            # (e.g. command: [foo]) would pass a bare truthiness check and
            # later render into invalid native configuration.
            if not isinstance(command, str) or not command.strip():
                raise ValidationError(
                    f"Event '{event_name}' missing required 'command' string"
                )
            if event_name not in CANONICAL_EVENTS:
                raise ValidationError(
                    f"Unknown event '{event_name}': "
                    f"must be one of {sorted(CANONICAL_EVENTS)}"
                )
            # C10: matcher must be a string (or absent). A non-string matcher
            # such as `matcher: []` would later crash by_matcher.setdefault.
            matcher = event_config.get("matcher")

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Wrap each event's value in a mapping with at least a `command` key.
  2. Check indentation: the `command:` line must be indented under the event name, not at the same column.
  3. Re-run specify extension install/refresh.

Example fix

# before
events:
  pre_tool_use: ./hook.sh

# after
events:
  pre_tool_use:
    command: "./hook.sh"
Defensive patterns

Strategy: type-guard

Validate before calling

for name, cfg in (manifest.get("events") or {}).items():
    assert isinstance(cfg, dict), f"event {name} config must be a mapping"

Type guard

def all_event_configs_are_dicts(events: dict) -> bool:
    return all(isinstance(c, dict) for c in events.values())

Try / catch

except ValidationError as e:
    if "Invalid event" in str(e) and "expected a mapping" in str(e):
        wrap_command_string(name, cfg)  # convert 'cmd' shorthand to {command: cmd}

Prevention

When it happens

Trigger: Manifest has `events: {pre_tool_use: "./hook.sh"}` (string instead of config mapping) or `events: {pre_tool_use:}` (null), and validate_events iterates event items and finds a non-dict event_config.

Common situations: Trying the shorthand `event: command-string` instead of `event: {command: ...}`; leaving an event section written but empty (YAML null); wrong indentation nesting the config one level off.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/a04112a173edcfad. Report an issue: GitHub.