Skip to content

Instantly share code, notes, and snippets.

@oliver-kriska
Created January 8, 2026 10:36
Show Gist options
  • Select an option

  • Save oliver-kriska/1cce5dcc0fef1f4ce3d9e941d8e1f8be to your computer and use it in GitHub Desktop.

Select an option

Save oliver-kriska/1cce5dcc0fef1f4ce3d9e941d8e1f8be to your computer and use it in GitHub Desktop.
Claude Code macOS Notifications - Get notified when Claude needs your attention

Claude Code macOS Notifications

Get native macOS notifications when Claude Code needs your attention - questions, permissions, idle timeouts, and task completions.

Features

  • Different sounds for different notification types (Glass, Sosumi, Purr, Hero)
  • Persistent alerts for urgent notifications (questions, permissions)
  • Auto-dismiss banners for task completions
  • Click to focus - clicking notification brings your terminal to front
  • Auto-detects terminal - Works with Ghostty, Zed, iTerm2, Terminal.app, VS Code, Kitty, WezTerm
  • Notification grouping - Prevents notification spam
  • Smart skip - Won't notify if terminal is already focused

Preview

Event Sound Style When
Question Glass Persistent Claude asks you something
Permission Sosumi Persistent Claude needs tool approval
Idle Purr Persistent (once) Waiting 60+ seconds
Task Done Hero Banner Task completed
Subagent Done Hero Banner Background agent finished

Installation

1. Install terminal-notifier

brew install terminal-notifier

2. Create the notification script

mkdir -p ~/.claude
curl -o ~/.claude/notify.sh https://gist.githubusercontent.com/YOUR_USERNAME/GIST_ID/raw/notify.sh
chmod +x ~/.claude/notify.sh

Or manually create ~/.claude/notify.sh with the content from notify.sh.

3. Add hooks to your settings

Edit ~/.claude/settings.json and add the hooks configuration. If the file doesn't exist, create it.

If you already have a settings.json, merge the hooks section from settings-hooks.json into your existing file.

If you don't have one, you can use the hooks file as a starting point:

# Only if you don't have settings.json yet:
cp ~/.claude/settings.json ~/.claude/settings.json.backup 2>/dev/null
curl -o /tmp/hooks.json https://gist.githubusercontent.com/YOUR_USERNAME/GIST_ID/raw/settings-hooks.json
# Then manually merge the hooks into your settings.json

4. Configure macOS notification permissions

  1. Open System Settings → Notifications
  2. Find terminal-notifier and enable notifications
  3. Set Alert Style to Alerts (for persistent notifications)
  4. Enable Play sound for notifications

5. Restart Claude Code

# Exit current session
/exit

# Start new session
claude

Testing

Test the notification script directly:

# Test question notification
~/.claude/notify.sh "Test question" "Claude Code" "question"

# Test permission notification
~/.claude/notify.sh "Test permission" "Claude Code" "permission"

# Test done notification
~/.claude/notify.sh "Test done" "Claude Code" "done"

Customization

Change sounds

Edit ~/.claude/notify.sh and modify the sound names. Available macOS sounds:

# List all available sounds
ls /System/Library/Sounds/

# Preview a sound
afplay /System/Library/Sounds/Glass.aiff

Add more terminals

Edit the case "$TERM_PROGRAM" section in notify.sh to add your terminal's bundle identifier:

# Find your terminal's bundle ID
osascript -e 'id of app "YourTerminalName"'

Troubleshooting

Notifications not showing

  • Check System Settings → Notifications → terminal-notifier is enabled
  • Make sure Focus/Do Not Disturb is off

No sound

  • Check "Play sound for notifications" is enabled in notification settings
  • Verify sound name exists: ls /System/Library/Sounds/

Click doesn't focus terminal

  • The script auto-detects your terminal from $TERM_PROGRAM environment variable
  • If your terminal isn't detected, add it to the case statement in notify.sh

Notifications disappear too fast

  • Set Alert Style to Alerts (not Banners) in System Settings → Notifications → terminal-notifier

Credits

Based on community best practices from:

License

MIT - Feel free to use, modify, and share!

#!/bin/bash
# Claude Code notification script - auto-detects terminal app
# https://gist.github.com/YOUR_USERNAME/GIST_ID
MESSAGE="${1:-Waiting for your input}"
TITLE="${2:-Claude Code}"
TYPE="${3:-default}" # question, permission, idle, done
# Choose sound and subtitle based on notification type
case "$TYPE" in
question)
SOUND="Glass"
SUBTITLE="Question"
GROUP="claude-code-question"
;;
permission)
SOUND="Sosumi"
SUBTITLE="Permission Required"
GROUP="claude-code-permission"
;;
idle)
SOUND="Purr"
SUBTITLE="Idle"
GROUP="claude-code-idle"
;;
done)
SOUND="Hero"
SUBTITLE="Completed"
GROUP="claude-code-done"
;;
*)
SOUND="default"
SUBTITLE=""
GROUP="claude-code"
;;
esac
# Detect terminal app from environment
APP_ID=""
# Check TERM_PROGRAM first (Ghostty, iTerm, Terminal, VSCode, Kitty, WezTerm)
if [[ -n "$TERM_PROGRAM" ]]; then
case "$TERM_PROGRAM" in
ghostty) APP_ID="com.mitchellh.ghostty" ;;
Apple_Terminal) APP_ID="com.apple.Terminal" ;;
iTerm.app) APP_ID="com.googlecode.iterm2" ;;
vscode) APP_ID="com.microsoft.VSCode" ;;
Zed) APP_ID="dev.zed.Zed" ;;
WezTerm) APP_ID="com.github.wez.wezterm" ;;
kitty) APP_ID="net.kovidgoyal.kitty" ;;
esac
fi
# Check __CFBundleIdentifier (Zed and others set this)
if [[ -z "$APP_ID" && -n "$__CFBundleIdentifier" ]]; then
APP_ID="$__CFBundleIdentifier"
fi
# Check ZED environment variable
if [[ -z "$APP_ID" && -n "$ZED" ]]; then
APP_ID="dev.zed.Zed"
fi
# Skip notification if terminal is already focused (for non-urgent types)
if [[ "$TYPE" == "done" || "$TYPE" == "idle" ]]; then
FRONTMOST=$(osascript -e 'tell application "System Events" to get bundle identifier of first application process whose frontmost is true' 2>/dev/null)
if [[ "$FRONTMOST" == "$APP_ID" ]]; then
exit 0
fi
fi
# Build notification command
NOTIFY_CMD=(terminal-notifier -title "$TITLE" -message "$MESSAGE" -sound "$SOUND" -group "$GROUP")
# Add subtitle if present
if [[ -n "$SUBTITLE" ]]; then
NOTIFY_CMD+=(-subtitle "$SUBTITLE")
fi
# Add activate if APP_ID is known
if [[ -n "$APP_ID" ]]; then
NOTIFY_CMD+=(-activate "$APP_ID")
fi
# For "done" type, use banner style (disappears automatically)
# For others, use alert style (persistent)
if [[ "$TYPE" != "done" ]]; then
NOTIFY_CMD+=(-actions "Open" -timeout 0)
fi
# Execute notification
"${NOTIFY_CMD[@]}"
{
"hooks": {
"PreToolUse": [
{
"matcher": "AskUserQuestion",
"hooks": [
{
"type": "command",
"command": "~/.claude/notify.sh 'Waiting for your input' 'Claude Code' 'question'"
}
]
}
],
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{
"type": "command",
"command": "~/.claude/notify.sh 'Needs permission' 'Claude Code' 'permission'"
}
]
},
{
"matcher": "idle_prompt",
"hooks": [
{
"type": "command",
"command": "~/.claude/notify.sh 'Still waiting for you...' 'Claude Code' 'idle'",
"once": true
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/notify.sh 'Task completed' 'Claude Code' 'done'"
}
]
}
],
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/notify.sh 'Subagent finished' 'Claude Code' 'done'"
}
]
}
]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment