Claude Code hooks
The Notification hook in Claude Code
Notification fires when Claude Code sends a notification. Match on notification_type to get alerted only when a session actually needs you.
permission_prompt overlaps with PermissionRequest, but the two are not interchangeable. PermissionRequest can still decide the outcome before anything is shown; this one only learns that a prompt already appeared, after the fact, and has no way to influence it.
- When it fires
- When Claude Code sends a notification.
- Can it block?
- No.
- Matcher
- Yes, on
notification_type - Matcher examples
permission_promptidle_promptauth_successagent_needs_inputagent_completed
What it receives
The event arrives as JSON on standard input. These are the fields worth reading.
| Field | Type | What it is |
|---|---|---|
notification_type | string | Which notification. Also what the matcher filters on. |
message | string | The notification text. |
session_id | string | Stable for the session. |
Exit codes
The exit code is the decision. Anything the script writes to standard error on a blocking exit is what Claude gets told.
| Exit | What happens |
|---|---|
0 | Success. |
2 | No blocking behavior. |
other | Non-blocking. |
What it can return
Nothing returned here changes anything. This event exists so you can route notifications somewhere you will actually see them.
No decision fields.
Getting told when a long run is waiting on you
The reason to match a specific notification_type is that agent_needs_input means a run has stalled on a question, which is worth interrupting you for, while most other notifications are not.
In settings.json
{
"hooks": {
"Notification": [
{
"matcher": "agent_needs_input|permission_prompt",
"hooks": [
{ "type": "command", "command": "node ~/.claude/hooks/notify.mjs" }
]
}
]
}
}
The script
const fs = require('fs');
const input = JSON.parse(fs.readFileSync(0, 'utf8')) || {};
// ntfy.sh needs no account. Pick a topic, subscribe to it in the phone app,
// and put the same string in NTFY_TOPIC.
const topic = process.env.NTFY_TOPIC;
if (!topic) process.exit(0);
const req = require('https').request(
{ hostname: 'ntfy.sh', path: '/' + topic, method: 'POST' },
() => process.exit(0)
);
// A notifier must never hold up a session. The default hook timeout is 600
// seconds, which is long enough for a dead host to look like a hung Claude.
req.setTimeout(3000, () => process.exit(0));
req.on('error', () => process.exit(0));
req.end(input.message || 'Claude Code needs you');
The pipe in the matcher is an exact-match list, not a regular expression, because the string contains only letters, underscores and pipes.
The catch
This event fires often. Anything slow or chatty attached to it becomes noise you learn to ignore, which defeats the point of having it.