Every alert, shown or not
Claude Code emits alerts you never see on screen. Notification fires whenever one of them happens, whether or not the desktop notification is displayed. The matcher filters twelve types: permission_prompt, idle_prompt, auth_success, elicitation_dialog, elicitation_url_dialog, elicitation_complete, elicitation_response, agent_needs_input, agent_completed, quota_auto_resume_fired, quota_auto_resume_stale, and quota_auto_resume_disabled. Omit it and all twelve run the handler.
They fall into four groups: a permission prompt waiting on you, the session going idle, an MCP server’s elicitation dialog at each stage, and a quota-driven auto-resume in one of three states. A hook that only wants to know about an unanswered prompt matches permission_prompt and sleeps through the other eleven. The payload carries message, an optional title, and notification_type.
The only output that does anything
You cannot block or rewrite the alert. No exit code changes the notification. On exit 0, stdout and stderr go to the debug log. Exit 2 is ignored, exit status and stderr alike. On every code a valid terminalSequence still fires, producing whatever terminal effect the hooks reference allows for it. systemMessage and continue are discarded, and there is no decision field.
You return {"systemMessage": "Claude needs you"} on permission_prompt, expecting a line on screen.
Nothing appears. The field is discarded on this event, as is everything else the hook prints. The prompt sits there as it would have without the hook.
Use terminalSequence for the terminal, or reach outside it: a desktop notifier or a webhook, a web address you call to notify another service, called from inside the hook is a side effect, and a side effect is the only thing this event can produce.