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 on to 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.

  1. Open Script Editor
  2. 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 jq to extract the message
  • PROJECT captures the current directory name so you know which project the notification is from
  • The say command 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.

References