Add a Notification hook with the matcher permission_prompt to ~/.claude/settings.json. Claude Code runs it when a tool approval has waited about six seconds. The idle_prompt matcher covers the other case: Claude finished about a minute ago and you haven't typed since. Below: the exact config, every notification type Claude Code sends, one hook for everything that waits on you, the timing rules, your phone, and fixes for a hook that never fires. The Claude Code notifier wires these hooks for you.
Rather skip the manual setup? One command wires every agent you have installed to desktop toasts, phone push, and Slack, Discord or Telegram:
Updated 2026-10-16 · also see the Claude Code notifier and the full docs
This macOS example shows a different banner for an approval and for an idle session. Each matcher gets its own entry under Notification:
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs your approval\" with title \"Claude Code\"'"
}
]
},
{
"matcher": "idle_prompt",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude is waiting for you\" with title \"Claude Code\"'"
}
]
}
]
}
}| Platform | Command | Watch out for |
|---|---|---|
| macOS | osascript -e 'display notification "…" with title "Claude Code"' | Banners come from Script Editor. Without its notification permission the command fails silently: run it once, then allow Script Editor in System Settings > Notifications |
| Linux | notify-send 'Claude Code' 'Claude needs your approval' | Needs a desktop notification daemon, which headless servers, SSH sessions and most containers don't have |
| Windows | PowerShell, with a module such as BurntToast for a real toast | Claude Code's own example uses a MessageBox, a dialog box that can open behind your terminal |
Type /hooks in Claude Code and select Notification to confirm the hooks are registered. Leave the matcher empty, or leave it out, and one hook runs for every notification type.
| Matcher | Fires when | Waiting on you? |
|---|---|---|
permission_prompt | A tool approval, or a sandboxed command's network request (v2.1.246+ in a terminal), has waited about six seconds | Yes |
idle_prompt | Claude finished about 60 seconds ago, you haven't typed since, and no background agent is still running | Claude is done; nothing is blocked |
elicitation_dialog | An MCP server opened a form and you haven't typed for about six seconds | Yes |
elicitation_url_dialog | An MCP server asks you to open a URL and you haven't typed for about six seconds | Yes |
agent_needs_input | A background session waits on you while agent view is open (v2.1.198+). Also: a terminal session shows an agent team teammate's setup question (v2.1.248+) or auto mode's notice about classifier request charges, once you haven't typed for about six seconds | Yes |
agent_completed | A background session finished or failed, while agent view is open (v2.1.198+) | No |
quota_auto_resume_stale | A usage limit reset while your computer slept, and Claude waits for Enter (v2.1.234+) | Yes |
auth_success | Authentication completed | No |
elicitation_complete, elicitation_response | An MCP elicitation finished, or its response was sent | No |
quota_auto_resume_fired, quota_auto_resume_disabled | Claude continued after a usage limit, or stopped waiting for one (v2.1.234+) | No |
The hook gets the event as JSON on stdin. Besides the common fields (session_id, transcript_path, cwd), a Notification hook receives message, an optional title, and notification_type, so one script can branch on the type instead of using several matchers:
{
"session_id": "abc123",
"transcript_path": "/Users/you/.claude/projects/…/session.jsonl",
"cwd": "/Users/you/my-app",
"hook_event_name": "Notification",
"message": "Claude needs your permission",
"title": "Permission needed",
"notification_type": "permission_prompt"
}A matcher can list several notification types separated by |, so one entry covers approvals and MCP questions together. Claude Code treats a matcher made only of letters, digits, _, -, spaces, , and | as an exact list, not a regular expression:
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt|elicitation_dialog|elicitation_url_dialog",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code is waiting for you\" with title \"Claude Code\"'"
}
]
}
]
}
}To give each type its own banner from one script, read notification_type from stdin. This Linux script needs jq and notify-send; on macOS replace each notify-send line with osascript -e 'on run argv' -e 'display notification (item 1 of argv) with title "Claude Code"' -e 'end run' "$msg", which passes the message as an argument so a quote in it cannot break the AppleScript. Save it as ~/.claude/hooks/needs-you.sh:
#!/bin/sh
input=$(cat)
type=$(printf '%s' "$input" | jq -r '.notification_type')
msg=$(printf '%s' "$input" | jq -r '.message')
case "$type" in
permission_prompt|elicitation_dialog|elicitation_url_dialog)
notify-send -u critical -- 'Claude Code needs you' "$msg" ;;
idle_prompt)
notify-send -- 'Claude Code is waiting' "$msg" ;;
esacRegister it under Notification with no matcher, so the script sees every type and ignores the ones it has no branch for, such as auth_success:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "sh ~/.claude/hooks/needs-you.sh"
}
]
}
]
}
}permission_prompt waits about six seconds after the prompt appears, so a prompt you answer right away never pings you. Each keystroke pushes that timer back.CLAUDE_CODE_DISABLE_PERMISSION_PROMPT_NOTIFY_HOOKS=1 to turn it off in those hosts.Stop hook, not idle_prompt. See the Claude Code notifications guide.PermissionRequest hook. It runs before the prompt is shown and can return a decision (allow or deny); in an interactive session, a hook that returns no decision leaves the normal prompt in place.Away from the desk, a banner doesn't help. ntfy is a free push service with Android and iOS apps and no account: subscribe to a topic name only you know, then POST to it from the hook. An urgent priority makes the push stand out:
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{
"type": "command",
"command": "curl -s -H 'Title: Claude Code' -H 'Priority: urgent' -H 'Tags: warning' -d 'Claude needs your approval' https://ntfy.sh/your-secret-topic"
}
]
}
]
}
}Anyone who knows a public ntfy.sh topic can read it, so send generic text only. The ntfy guide covers private topics and self-hosting.
echo '{"notification_type":"permission_prompt","message":"test"}' | sh ~/.claude/hooks/needs-you.sh. If no banner appears, the problem is the command (a missing jq, notify-send or Script Editor permission), not Claude Code./hooks in Claude Code and select Notification. The matcher must be one of the types above, spelled exactly.permission_prompt only runs after the prompt has waited about six seconds, and an answer from you before that cancels it (in Claude Desktop and VS Code, an answer from a PermissionRequest hook does too). In a terminal, each keystroke while the prompt is open pushes the timer back again.CLAUDE_CODE_DISABLE_PERMISSION_PROMPT_NOTIFY_HOOKS is not set to 1.permission_prompt covers it from v2.1.246. A PermissionRequest hook never runs for it, so use permission_prompt there.preferredNotifChannel is notifications_disabled; that setting only changes how Claude Code alerts you itself.npx anotifier@latest setupSetup registers one Notification hook with an empty matcher, plus a Stop hook, in ~/.claude/settings.json. Every notification becomes a needs-input alert on your desktop, your phone (if you turn on ntfy), and your webhook, with the project in the title (my-app · Claude Code) and, in the toast and webhook, the question Claude asked.
| Notification | Priority | Sound | ntfy tags |
|---|---|---|---|
| The idle reminder ("Claude is waiting for your input") | default | Reminder | hourglass_flowing_sand |
| Any other notification | urgent | Reminder | bell, warning |
Only the idle reminder is toned down, because nothing is blocked. To pick its level, set idleReminderPriority to min, low, default, high, or urgent:
{ "events": { "needs_input": { "idleReminderPriority": "low" } } }The Claude Code claims on this page come from the official docs, read on 2026-10-16 against the latest Claude Code release (2.1.301). Notification types, timings and version cut-offs change between releases, so confirm anything you rely on there.
PermissionRequest, matcher evaluation, /hooks: Hooks referencepreferredNotifChannel: Settings referenceAdd a Notification hook with "matcher": "permission_prompt" to ~/.claude/settings.json and give it a command that shows a banner, plays a sound, or sends a push. It runs when the prompt has waited about six seconds.
Claude Code waits about six seconds so that prompts you answer straight away never notify you. In a terminal, typing pushes the timer back. For a signal the instant Claude asks, use a PermissionRequest hook.
permission_prompt is a Notification type: it fires after about six seconds and can only trigger side effects. PermissionRequest is its own hook event: it runs before the prompt appears and can allow or deny the request. It does not run for a sandboxed command's network request.
That is idle_prompt: Claude Code sends it about 60 seconds after a turn ends if you haven't typed. Use a Stop hook to be told the moment Claude finishes, and leave idle_prompt out of your matchers if you don't want the reminder.
Not with anotifier today: the push gets you back to the keyboard, and you answer in the session. A PermissionRequest hook can return allow or deny, which is what a remote-approval tool would build on.
Use a Notification hook. permission_prompt runs when an approval has waited about six seconds, elicitation_dialog when an MCP server opens a form, and idle_prompt about 60 seconds after Claude finishes if you haven't typed. List several in one matcher with a pipe, such as permission_prompt|elicitation_dialog. For the moment a turn ends, use a Stop hook.
Yes. A matcher of letters, digits and pipes is an exact list, so "permission_prompt|elicitation_dialog" runs for either. Or leave the matcher out and have the script read notification_type from stdin and branch on it.
Usually you answered within about six seconds, or you are in Claude Desktop or the VS Code extension on a version before v2.1.233, or CLAUDE_CODE_DISABLE_PERMISSION_PROMPT_NOTIFY_HOOKS is set to 1. Pipe a sample event into the command to rule out the command itself, and check /hooks.
One command wires every agent you have installed: Claude Code, Codex CLI, Cursor, and Gemini CLI.
npx anotifier@latest setup