A small set of hooks that post native macOS Notification Center alerts — with sound — when Claude Code finishes a task, waits for your approval, hits an error, or compacts its context. Works everywhere, including the VSCode integrated terminal.
Why a hook? Claude Code's built-in
preferredNotifChannelsetting only shows desktop notifications in iTerm2 / Ghostty / Kitty — VSCode and most other terminals aren't on that list. A hook bypasses the terminal entirely and posts straight to macOS.
Zero tokens / zero cost. These are
"type": "command"hooks — they run a plain shell command and send nothing to the model (LLM). No API call → no tokens → no cost. (Onlypromptandagenthook types consume tokens; none are used here.)
Open ~/.claude/settings.json and add the hooks block below.
hooks field, add these events inside your existing
hooks object.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Task complete\" with title \"Claude Code\" sound name \"Glass\"'"
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Waiting for your input\" with title \"Claude Code\" sound name \"Ping\"'"
}
]
}
],
"StopFailure": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Something went wrong\" with title \"Claude Code\" sound name \"Basso\"'"
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Compacting context\" with title \"Claude Code\" sound name \"Purr\"'"
}
]
}
]
}
}osascript notifications are attributed to the "Script Editor" app. If that
app has never delivered a notification, macOS won't list it under
Notifications and the alerts get silently dropped (especially on macOS 15 / 26).
Classic chicken-and-egg.
Fix — run this once in your terminal:
open -a "Script Editor"
osascript -e 'display notification "test" with title "Claude Code"'Then: System Settings → Notifications → Script Editor → "Allow Notifications". It works permanently after that; you can close the Script Editor window.
After editing the settings file, run /hooks once in Claude Code (reloads the
config) or restart.
| Event | Fires when | Message | Sound |
|---|---|---|---|
Stop |
Claude finishes a turn | Task complete | Glass |
Notification |
Waiting for approval / attention | Waiting for your input | Ping |
StopFailure |
A turn ends with an error | Something went wrong | Basso |
PreCompact |
Context is auto-compacted | Compacting context | Purr |
Distinct sounds → you can tell which one fired without looking.
Change the sound: edit the sound name "..." part. Built-in system sounds
live in /System/Library/Sounds/:
Basso, Blow, Bottle, Frog, Funk, Glass, Hero, Morse, Ping, Pop, Purr, Sosumi, Submarine, Tink
Change the message: edit the text inside the first pair of quotes.
- These hooks only show a local notification — they read no files, make no network calls, and require no admin (sudo) privileges. The messages are static strings, so there's no command-injection surface (no external data is interpolated into the command).
commandhooks run a shell command. Always read what a hook does before pasting it from any source — this is general Claude Code hygiene, not specific to this snippet.- Don't share your whole
settings.json; share only thehooksblock. The file may later contain permission rules, env vars, or credential helpers.
- macOS (osascript is built in; nothing to install)
- Claude Code
- Troubleshooting: if nothing appears, check Focus / Do Not Disturb first — it suppresses both the banner and the sound while active.