{"record":{"id":"d4c69c65ba30efbe","repo":"github/spec-kit","slug":"event-event-name-handler-has-invalid-matcher","errorCode":null,"errorMessage":"Event '{event_name}' handler has invalid 'matcher': must be a string","messagePattern":"Event '(.+?)' handler has invalid 'matcher': must be a string","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/events.py","lineNumber":816,"sourceCode":"    \"\"\"\n    from .extensions import ValidationError\n\n    if event_name not in CANONICAL_EVENTS:\n        raise ValidationError(\n            f\"Unknown event '{event_name}': must be one of {sorted(CANONICAL_EVENTS)}\"\n        )\n    for handler in handlers:\n        command = handler.get(\"command\")\n        if not isinstance(command, str) or not command.strip():\n            raise ValidationError(\n                f\"Event '{event_name}' handler missing required non-empty 'command' string\"\n            )\n        # C10: matcher must be a string (or absent). A non-string matcher such\n        # as `matcher: []` passes extension validation but later crashes\n        # by_matcher.setdefault(matcher, ...) with TypeError: unhashable type.\n        matcher = handler.get(\"matcher\")\n        if matcher is not None and not isinstance(matcher, str):\n            raise ValidationError(\n                f\"Event '{event_name}' handler has invalid 'matcher': \"\n                \"must be a string\"\n            )\n        timeout = handler.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}' handler has invalid 'timeout': must be a positive integer\"\n                )\n\n\ndef resolve_events(\n    integration_key: str,\n    integration_config: dict[str, Any] | None,\n    project_root: Path,\n    parsed_options: dict[str, Any] | None,\n) -> ResolvedEvents:\n    \"\"\"Resolve the final event set for an integration.","sourceCodeStart":798,"sourceCodeEnd":834,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/events.py#L798-L834","documentation":"Raised while resolving an integration's declared event handlers: a handler entry in an extension's events config supplied a 'matcher' key whose value is neither a string nor null. The check exists (issue C10) because a non-string, unhashable matcher such as `matcher: []` passes earlier validation but later crashes `by_matcher.setdefault(matcher, ...)` with TypeError: unhashable type.","triggerScenarios":"An extension declares `events:` for an integration where a handler entry is a mapping containing `matcher: []`, `matcher: 123`, or `matcher: {a: b}`; resolve_events() -> handler validation loop hits the non-string matcher and raises ValidationError before any config is written.","commonSituations":"Authoring an extension manifest (extension.yaml) with a YAML list or number for matcher instead of a quoted string; copy-pasting a matcher syntax from a different tool that accepts arrays/patterns; YAML unquoted values parsing to non-string types.","solutions":["Change the matcher value in the handler entry to a plain string, e.g. `matcher: \"tool_use\"`, or delete the matcher key entirely if no filtering is needed.","Re-run the extension install/refresh; resolve_events re-validates and should pass.","If you intended multiple matchers, split into one handler entry per matcher string."],"exampleFix":"# before (extension.yaml)\nevents:\n  pre_tool_use:\n    handlers:\n      - command: \"./check.sh\"\n        matcher: [write_file, edit_file]\n\n# after\nevents:\n  pre_tool_use:\n    handlers:\n      - command: \"./check.sh\"\n        matcher: \"write_file|edit_file\"","handlingStrategy":"validation","validationCode":"def valid_handler(handler: dict) -> bool:\n    m = handler.get(\"matcher\")\n    return m is None or isinstance(m, str)","typeGuard":null,"tryCatchPattern":"from specify_cli.extensions import ValidationError\ntry:\n    resolve_events(key, config, project_root, options)\nexcept ValidationError as e:\n    if \"matcher\" in str(e):\n        fix_manifest_and_retry()  # matcher must be a str or absent\n    raise","preventionTips":["Keep matcher as a single string; use the integration's own alternation syntax instead of a list.","Lint extension manifests in CI with yaml.safe_load plus isinstance checks before running specify."],"tags":["events","validation","yaml","extensions"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}