The Claude Code Notification hook runs whenever Claude Code sends a notification, most often because a permission prompt is waiting (permission_prompt) or because Claude finished and you haven't replied (idle_prompt). It is timed for when you seem to be away: permission_prompt fires once the prompt has waited about six seconds without a keystroke, and idle_prompt about 60 seconds after Claude finished. The hook gets the notification text as JSON, cannot block anything, and is meant for side effects such as a desktop alert or a push to your phone.
01The notification types
The matcher on a Notification hook is the notification type. Leave it empty to run on all of them, or list the ones you want with |. The types you are most likely to use:
permission_promptClaude needs you to approve a tool use (or a sandboxed command's network request) and the prompt has waited about six secondsidle_promptClaude finished responding about 60 seconds ago and you haven't typed sinceelicitation_dialogAn MCP server opened a form and you haven't typed for about six secondselicitation_url_dialogAn MCP server asked you to open a URL and you haven't typed for about six secondsauth_successAuthentication completedThere are more, such as agent_needs_input and agent_completed for background sessions in agent view, and a set of quota_auto_resume_* types for usage-limit waits. The hooks reference has the full, current list.
02The timing rules
This is the part that catches people out. The Notification hook shares its timing with Claude Code's own desktop notifications, so in a terminal session it only fires when you look away:
permission_promptstarts a timer of about six seconds when the prompt appears. Every keystroke resets it. If you answer quickly, it never fires.idle_promptfires about 60 seconds after Claude finishes, only if you haven't typed since and no background agent is still running. It doesn't fire while Claude Code waits for a usage limit to reset.- A request that arrives while another dialog is open is timed from when it arrives, so its notification can reach you while it still waits behind the open dialog.
Hosts that answer permissions through the Agent SDK, such as Claude Desktop and the VS Code extension, time permission_prompt differently: about six seconds after the request, not deferred by typing, and skipped if you or a PermissionRequest hook answer first.
If you want a signal with no delay, use a different event:
PermissionRequestStopNotification with idle_prompt03What the hook receives
On top of the common fields (session_id, transcript_path, cwd, hook_event_name), a Notification hook gets the notification's message, an optional title, and the notification_type:
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "Notification",
"message": "Claude needs your permission",
"title": "Permission needed",
"notification_type": "permission_prompt"
}
The message is Claude Code's own wording, so it is safe to show as is. cwd is useful when you run several sessions and want to know which project is asking.
04What the hook can and can't do
A Notification hook cannot block, delay or change the notification. Its exit code and stderr are ignored, and Claude Code discards systemMessage and continue from its JSON output. It is purely for side effects.
There is one output field it can use: terminalSequence. Hooks run without a terminal, so they can't write escape sequences themselves, but they can hand one to Claude Code to emit. That lets a hook trigger a terminal-native notification in iTerm2, Ghostty, Kitty, WezTerm, Windows Terminal and others, even inside tmux. It only works in an interactive session with Claude Code's interface on screen.
The preferredNotifChannel setting doesn't stop the hook either. Even with built-in notifications turned off, the Notification event still fires. That setting only changes how Claude Code itself alerts you; see the terminal bell guide.
05Examples
Different handlers per type
Run a separate script for permission prompts and for idle reminders:
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/permission-alert.sh" }]
},
{
"matcher": "idle_prompt",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/idle-alert.sh" }]
}
]
}
}
Make each script executable with chmod +x. The desktop notification commands for Mac, Windows and Linux are in how to get a notification when Claude Code finishes, so they aren't repeated here.
Show the real message and project
This script (macOS and Linux) puts Claude Code's message and the project folder in the notification, instead of a fixed string:
#!/bin/bash
# ~/.claude/hooks/notify.sh
input=$(cat)
msg=$(jq -r '.message // "Claude Code needs your attention"' <<<"$input")
project=$(basename "$(jq -r '.cwd // "."' <<<"$input")")
if command -v osascript >/dev/null 2>&1; then
osascript -e 'on run argv' \
-e 'display notification (item 1 of argv) with title (item 2 of argv)' \
-e 'end run' "$msg" "Claude Code: $project"
elif command -v notify-send >/dev/null 2>&1; then
notify-send "Claude Code: $project" "$msg"
fi
exit 0
Passing the text as arguments (on run argv) avoids breaking the AppleScript when the message contains quotes. It needs jq installed.
A terminal-native notification
Anthropic's own example uses terminalSequence with OSC 777, which Ghostty, Warp and urxvt understand:
#!/bin/bash
input=$(cat)
title="Claude Code"
body=$(jq -r '.message // "Needs your attention"' <<<"$input")
seq=$(printf '\033]777;notify;%s;%s\007' "$title" "$body")
jq -nc --arg seq "$seq" '{terminalSequence: $seq}'
For iTerm2, Windows Terminal or WezTerm use OSC 9 instead, and for Kitty OSC 99. Anything outside the allowed sequences is ignored.
06Where Claude Code Notifier fits
A Notification hook that calls a push service gets the alert to your phone, and for many people that is enough. The ntfy guide shows that route.
The gap is the reply. By the time permission_prompt fires, Claude is waiting on a dialog that a Notification hook can't answer. Claude Code Notifier handles this with the hooks that can: PermissionRequest to return your decision and PreToolUse on AskUserQuestion to return your answer. Its free command line tool, ccn, runs on Mac, Windows and Linux and shows desktop notifications on its own. With the iPhone or Android app paired, you can allow once, allow for the session, or deny from the phone. If you typed in the last 30 seconds, nothing waits for the phone and the terminal asks as usual. It is independent and not made by Anthropic. Installation is on the ccn page.
07FAQ
Why does my Notification hook never fire?
Most often because you are still at the keyboard. Each keystroke defers permission_prompt, and idle_prompt only fires if you haven't typed for about a minute after Claude finished. To test, trigger a permission prompt, switch to another window and wait at least six seconds. If it still doesn't fire, check /hooks lists it and see the troubleshooting checklist.
What is idle_prompt?
It is the notification type Claude Code sends when Claude finished responding about 60 seconds ago and you haven't typed since. It is a reminder that it is your turn, not an instant "done" signal. For that, use a Stop hook.
Can a Notification hook approve a permission?
No. Notification hooks have no decision control, so nothing they print can answer the prompt. Use a PermissionRequest hook to allow or deny, as described in the PermissionRequest guide.
Does the Notification hook run if I set preferredNotifChannel to notifications_disabled?
Yes. That setting only changes how Claude Code alerts you itself. The Notification event and your hooks still run.
Why do I get two notifications?
Claude Code's built-in notification and your hook run side by side. In iTerm2, Ghostty or Kitty you may see both. Turn one off, for example with preferredNotifChannel, or keep only the hook.