TL;DR: Set up a Claude Code hook to get macOS desktop notifications with sound and speech when Claude Code finishes a task or waits for input. SAY!!
Background
Claude Code sometimes runs tasks for a while, and you want to know when it’s done, right?
There’s an official notification setting, but notifications don’t always fire or are easy to miss.
So let’s use a custom hook to get reliable macOS desktop notifications with text-to-speech.
Try Terminal Native Notifications First
Before setting up hooks, check if your terminal supports native notifications:
- Kitty / Ghostty — Native notification support, no configuration needed
- iTerm2 — Go to Settings → Profiles → Terminal, enable “Notification Center Alerts” and check “Send escape sequence-generated alerts”
- Inside tmux — Set
set -g allow-passthrough onto forward notifications to the outer terminal (iTerm2, Kitty, Ghostty)
If that’s enough for you, no hook setup needed. Read on if you want more customization.
Preparation: Enable Script Editor Notifications
First, verify that osascript notifications work on your machine.
- Open Script Editor
- Create a new script and run:
display notification "test notification" with title "test title" subtitle "test subtitle" sound name "Glass"
If you hear the sound but don’t see the notification, go to System Settings > Notifications, find “Script Editor”, and enable notifications.
Create the Hook Script
Create ~/.claude/scripts/hooks/notification/desktop-notification.sh.
The script uses jq to parse JSON from the hook’s stdin. Install it first if you don’t have it:
brew install jq
#!/usr/bin/env bash
#
# Input-waiting notification (macOS only)
#
PROJECT=$(basename "$(pwd)")
# Read JSON from hook's stdin and extract the message
if [ ! -t 0 ] || [ -p /dev/stdin ]; then
INPUT=$(cat -)
# Use jq to extract .message from JSON if available
if command -v jq > /dev/null 2>&1; then
MESSAGE=$(echo "$INPUT" | jq -r '.message // empty' 2>/dev/null)
fi
MESSAGE=${MESSAGE:-"Waiting for your next instruction"}
else
MESSAGE=${1:-"Waiting for your next instruction"}
fi
osascript -e "display notification \"$MESSAGE\" with title \"Claude Code: $PROJECT\" sound name \"Pluck\""
if echo "$MESSAGE" | grep -qE '^[a-zA-Z0-9!\\?: ]+$'; then
# English message
say -v "Samantha (Enhanced)" "$MESSAGE" || say -v "Samantha" "$MESSAGE" || true
else
# Change voice in Settings > Accessibility > Spoken Content > System Voice
say "$MESSAGE" || true
fi
Then make it executable:
chmod +x ~/.claude/scripts/hooks/notification/desktop-notification.sh
Key points:
- Hook stdin receives JSON data, so we use
jqto extract the message PROJECTcaptures the current directory name so you know which project the notification is from- The
saycommand provides text-to-speech, so you’ll notice even when you’re not looking at the screen - Voice selection switches between English and Japanese messages
Configure Claude Code
Register the hook in ~/.claude/settings.json. Use the Notification event, which fires when Claude is waiting for user input.
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/scripts/hooks/notification/desktop-notification.sh"
}
]
}
]
}
}
The Notification event fires when Claude is waiting for user input, so this alone is sufficient. The script reads JSON from stdin and extracts the message itself.
If you also want notifications on API errors (rate limits, etc.), add the StopFailure event:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/scripts/hooks/notification/desktop-notification.sh"
}
]
}
],
"StopFailure": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/scripts/hooks/notification/desktop-notification.sh 'An error occurred'"
}
]
}
]
}
}
Alternative: Write It in CLAUDE.md
If setting up hooks feels like too much work, you can write it directly in ~/.claude/CLAUDE.md:
## Notify when waiting for user input or when task is complete
Notify the user whenever Claude Code finishes execution, regardless of whether it's waiting for input or has completed a task.
Use the following command for notification:
\```
osascript -e 'display notification "Waiting for your next instruction" with title "Claude Code" sound name "Pluck"'
\```
However, this approach relies on the model’s context, so notifications may not always fire. Hooks are more reliable.
Summary
That’s how you can get macOS notifications when Claude Code is waiting for input. It’s pretty convenient — you can work on other things while long tasks are running.