{"record":{"id":"6517a519d3c7a822","repo":"github/spec-kit","slug":"event-event-name-has-invalid-matcher-must-b","errorCode":null,"errorMessage":"Event '{event_name}' has invalid 'matcher': must be a string","messagePattern":"Event '(.+?)' has invalid 'matcher': must be a string","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/events.py","lineNumber":1805,"sourceCode":"                )\n            command = event_config.get(\"command\")\n            # #17: command must be a non-empty string. A truthy non-string\n            # (e.g. command: [foo]) would pass a bare truthiness check and\n            # later render into invalid native configuration.\n            if not isinstance(command, str) or not command.strip():\n                raise ValidationError(\n                    f\"Event '{event_name}' missing required 'command' string\"\n                )\n            if event_name not in CANONICAL_EVENTS:\n                raise ValidationError(\n                    f\"Unknown event '{event_name}': \"\n                    f\"must be one of {sorted(CANONICAL_EVENTS)}\"\n                )\n            # C10: matcher must be a string (or absent). A non-string matcher\n            # such as `matcher: []` would later crash by_matcher.setdefault.\n            matcher = event_config.get(\"matcher\")\n            if matcher is not None and not isinstance(matcher, str):\n                raise ValidationError(\n                    f\"Event '{event_name}' has invalid 'matcher': must be a string\"\n                )\n            timeout = event_config.get(\"timeout\")\n            if timeout is not None:\n                if not isinstance(timeout, int) or isinstance(timeout, bool) or timeout <= 0:\n                    raise ValidationError(\n                        f\"Event '{event_name}' has invalid 'timeout': must be a positive integer\"\n                    )\n\n\ndef has_events(data: dict[str, Any]) -> bool:\n    \"\"\"Return True if ``events`` is present and non-empty.\"\"\"\n    return bool(data.get(\"events\"))\n\n\n# -- Helper merging functions ----------------------------------------------\n\ndef _toml_quote(value: str) -> str:","sourceCodeStart":1787,"sourceCodeEnd":1823,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/events.py#L1787-L1823","documentation":"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.","triggerScenarios":"Manifest event config contains `matcher: []`, `matcher: 5`, or `matcher: {a: b}` and validate_events runs during extension install, list, or event refresh.","commonSituations":"Same shape as the resolver-side error: YAML list/number matcher, copied pattern syntax from other hook systems, unquoted YAML parsing surprises.","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."],"exampleFix":"# before\nevents:\n  pre_tool_use:\n    command: \"./hook.sh\"\n    matcher: [write, edit]\n\n# after\nevents:\n  pre_tool_use:\n    command: \"./hook.sh\"\n    matcher: \"write|edit\"","handlingStrategy":"validation","validationCode":"m = event_config.get(\"matcher\")\nif m is not None and not isinstance(m, str):\n    event_config[\"matcher\"] = \"|\".join(m) if isinstance(m, list) else str(m)","typeGuard":"def matcher_is_str_or_absent(cfg: dict) -> bool:\n    return cfg.get(\"matcher\") is None or isinstance(cfg[\"matcher\"], str)","tryCatchPattern":"except ValidationError as e:\n    if \"invalid 'matcher'\" in str(e):\n        flatten_matcher_to_string(cfg)","preventionTips":["Use one matcher string per handler; add duplicate handlers for multiple matchers.","Prefer regex alternation inside a single string over a list."],"tags":["events","validation","matcher","manifest"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}