{"record":{"id":"36eca8e324e00c65","repo":"github/spec-kit","slug":"event-event-name-has-invalid-timeout-must-b","errorCode":null,"errorMessage":"Event '{event_name}' has invalid 'timeout': must be a positive integer","messagePattern":"Event '(.+?)' has invalid 'timeout': must be a positive integer","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/events.py","lineNumber":1811,"sourceCode":"                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:\n    \"\"\"Render *value* as a TOML basic string via the shared escaper.\"\"\"\n    from ._toml_string import escape_toml_basic\n    return escape_toml_basic(value)\n\n\ndef _build_opencode_plugin(","sourceCodeStart":1793,"sourceCodeEnd":1829,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/events.py#L1793-L1829","documentation":"Manifest-level timeout validation in validate_events(): an event config's `timeout`, when present, must be a positive integer and not a boolean. Same rationale as the resolver-side check — bools are ints in Python and are explicitly excluded.","triggerScenarios":"Manifest event config has `timeout: 2.5`, `timeout: \"60\"`, `timeout: 0`, `timeout: -5`, or `timeout: false`; validate_events raises during manifest validation.","commonSituations":"Float seconds from another tool's config, quoted numbers, zero meaning 'no timeout' in the author's mind, YAML boolean coercion (`timeout: yes`).","solutions":["Use a bare positive integer: `timeout: 60`.","Drop the key entirely to accept the default.","Verify no quotes and no decimal point on the value."],"exampleFix":"# before\nevents:\n  pre_tool_use:\n    command: \"./hook.sh\"\n    timeout: 60.0\n\n# after\nevents:\n  pre_tool_use:\n    command: \"./hook.sh\"\n    timeout: 60","handlingStrategy":"validation","validationCode":"t = event_config.get(\"timeout\")\nif t is not None and (not isinstance(t, int) or isinstance(t, bool) or t <= 0):\n    event_config.pop(\"timeout\")  # or int(t) after asserting integral","typeGuard":"def timeout_ok(t) -> bool:\n    return t is None or (isinstance(t, int) and not isinstance(t, bool) and t > 0)","tryCatchPattern":"except ValidationError as e:\n    if \"invalid 'timeout'\" in str(e):\n        event_config[\"timeout\"] = int(float(raw)) if str(raw).replace('.','',1).isdigit() else default","preventionTips":["Bare integer literals only; no quotes, no decimals, no on/off words.","Omit the key rather than setting 0 to mean 'default'."],"tags":["events","validation","timeout","manifest"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}