github/spec-kit · error · ValidationError
Event '{event_name}' has invalid 'matcher': must be a string
Error message
Event '{event_name}' has invalid 'matcher': must be a string What it means
Manifest-level twin of the handler-level matcher check: validate_events() rejects an event config whose `matcher` is present but not a string (issue C10). A non-string matcher would later crash `by_matcher.setdefault(matcher, ...)` with TypeError: unhashable type, so it is stopped at validation time.
Source
Thrown at src/specify_cli/events.py:1805
)
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")
if matcher is not None and not isinstance(matcher, str):
raise ValidationError(
f"Event '{event_name}' has invalid 'matcher': must be a string"
)
timeout = event_config.get("timeout")
if timeout is not None:
if not isinstance(timeout, int) or isinstance(timeout, bool) or timeout <= 0:
raise ValidationError(
f"Event '{event_name}' has invalid 'timeout': must be a positive integer"
)
def has_events(data: dict[str, Any]) -> bool:
"""Return True if ``events`` is present and non-empty."""
return bool(data.get("events"))
# -- Helper merging functions ----------------------------------------------
def _toml_quote(value: str) -> str:View on GitHub (pinned to bf88c9f9a8)
Solutions
- Make matcher a single string (or remove the key).
- Express alternation inside one string if the integration's matcher syntax supports it (e.g. `matcher: "write|edit"`).
- Re-run specify extension install.
Example fix
# before
events:
pre_tool_use:
command: "./hook.sh"
matcher: [write, edit]
# after
events:
pre_tool_use:
command: "./hook.sh"
matcher: "write|edit" Defensive patterns
Strategy: validation
Validate before calling
m = event_config.get("matcher")
if m is not None and not isinstance(m, str):
event_config["matcher"] = "|".join(m) if isinstance(m, list) else str(m) Type guard
def matcher_is_str_or_absent(cfg: dict) -> bool:
return cfg.get("matcher") is None or isinstance(cfg["matcher"], str) Try / catch
except ValidationError as e:
if "invalid 'matcher'" in str(e):
flatten_matcher_to_string(cfg) Prevention
- Use one matcher string per handler; add duplicate handlers for multiple matchers.
- Prefer regex alternation inside a single string over a list.
When it happens
Trigger: Manifest event config contains `matcher: []`, `matcher: 5`, or `matcher: {a: b}` and validate_events runs during extension install, list, or event refresh.
Common situations: Same shape as the resolver-side error: YAML list/number matcher, copied pattern syntax from other hook systems, unquoted YAML parsing surprises.
Related errors
- Invalid events: expected a mapping
- Invalid event '{event_name}': expected a mapping
- Event '{event_name}' missing required 'command' string
- Event '{event_name}' has invalid 'timeout': must be a positi
- Manifest must be a YAML mapping at the top level.
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/6517a519d3c7a822.
Report an issue: GitHub.