Get a desktop notification when Claude Code finishes, with a Stop hook
Fire a notification the moment Claude finishes responding: commands for macOS, Linux and Windows, how Stop differs from Notification, and the exit-code trap to avoid.
Contents
Watching a terminal to see whether a long task has finished is wasted time. A notification at the moment it completes frees you to do something else in the meantime.
The event that fires when Claude finishes responding is the Stop hook. The Notification hook in the documentation's getting-started walkthrough is a different thing: it tells you Claude is waiting for input. This article keeps the two apart and gives the setup for all three platforms.
KEY POINT
What you will learn
- How
Stopdiffers fromNotification, and which one you want - The notification command and config for macOS, Linux and Windows
- The exit-code trap that can keep Claude from stopping
Stop or Notification?
The names are similar; the timing is not.
| Event | Fires when | Matcher |
|---|---|---|
Stop | Claude finishes responding | Not supported |
SubagentStop | A subagent finishes | Matches on agent type |
Notification | Claude Code sends a notification (permission prompt, idle, and so on) | Matches on notification type |
"Tell me when the work is done" is Stop. "Tell me when it's stuck on a permission prompt" is Notification. Configuring both is fine.
The notification types you can match on include permission_prompt, idle_prompt, auth_success, agent_needs_input and agent_completed. The full list of matcher values per event is in Every value you can put in a Claude Code hook matcher.
A matcher on Stop is ignored
Stop does not support a matcher. Adding one is not an error — it is silently ignored, and the hook fires every time. To narrow it, branch inside the hook script instead.
Configuration by platform
These go in ~/.claude/settings.json. Create the file if it does not exist.
macOS
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code finished\" with title \"Claude Code\"'"
}
]
}
]
}
}
If nothing appears, Script Editor — which osascript routes notifications through — probably lacks notification permission. The documentation notes that in that case the command fails silently and macOS never prompts you to grant it. Run this once in Terminal so Script Editor shows up in your notification settings, then enable Allow Notifications for Script Editor under System Settings > Notifications.
osascript -e 'display notification "test"'
Linux
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude Code finished'"
}
]
}
]
}
}
notify-send needs a desktop notification daemon, which headless servers, SSH sessions and most containers do not have. Test the command directly first. If it is not found, install libnotify-bin on Debian and Ubuntu, or your distribution's equivalent.
notify-send 'Claude Code' 'test'
Windows (PowerShell)
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code finished', 'Claude Code')\""
}
]
}
]
}
}
Run /hooks afterwards to see the configured hooks per event and which file each came from. Note that the /hooks menu is read-only: to add, modify or remove hooks, edit the settings JSON directly or ask Claude to do it.
The exit-code trap
This is the part specific to Stop. A Stop hook that exits with code 2 blocks the stop, and the conversation continues. If your notification command fails and returns 2, Claude keeps going when you did not intend it to.
When all you want is a notification, make sure the command exits 0.
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude Code finished' || true"
}
|| true turns a failed notification into a 0 exit.
There is also a cap on blocking. The documentation states that Claude Code overrides a Stop hook after it blocks eight times in a row without progress. If you write a hook that deliberately blocks, read stop_hook_active from the JSON on stdin and exit early once you have already triggered a continuation.
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Allow Claude to stop
fi
# ... rest of your hook logic
Putting content in the notification
A Stop hook's stdin carries last_assistant_message and stop_reason, so the notification can say what actually finished.
#!/usr/bin/env bash
# ~/.claude/hooks/notify-done.sh
INPUT=$(cat)
MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // empty' | head -c 120)
notify-send 'Claude Code' "${MSG:-Claude Code finished}" || true
exit 0
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/notify-done.sh"
}
]
}
]
}
}
For the full shape of that JSON, see The JSON your Claude Code hooks receive on stdin. For building hooks generally, see Run lint and format automatically after every edit.
用語解説
stop_reason: a string describing why Claude stopped. A normal end of turn carries end_turn.
Summary
- Use
Stopfor "work finished" andNotificationfor "waiting on you" Stoptakes no matcher; one written there is silently ignored- macOS uses
osascript, Linuxnotify-send, Windows a PowerShell MessageBox - A
Stophook exiting 2 blocks the stop; append|| truewhen you only want a notification - If you block deliberately, check
stop_hook_activeto stay under the eight-block cap last_assistant_messagelets the notification say what finished
FAQ
- What's the difference between the Stop and Notification hooks?
- Stop fires when Claude finishes responding. Notification fires when Claude Code sends a notification, such as a permission prompt or an idle prompt. For "tell me when it's done", use Stop.
- Can I put a matcher on a Stop hook?
- No. Stop does not support a matcher. Adding one is silently ignored, and the hook fires every time.
- Can a notification script stop Claude from finishing?
- Yes. A Stop hook that exits with code 2 blocks the stop and the conversation continues. If you only want a notification, make sure the command exits 0.
- Can I be notified when a subagent finishes?
- Yes, with the SubagentStop hook. Unlike Stop, it does accept a matcher on the agent type.
Primary sources
This article was drafted by AI from official documentation and reviewed by the site operator before publishing. Found a mistake? Let us know via the contact page.