jared hebb ~ %

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_prompt idle_prompt auth_success agent_needs_input agent_completed

What it receives

The event arrives as JSON on standard input. These are the fields worth reading.

FieldTypeWhat it is
notification_typestringWhich notification. Also what the matcher filters on.
messagestringThe notification text.
session_idstringStable 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.

ExitWhat happens
0Success.
2No blocking behavior.
otherNon-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.