Play a sound when Claude Code finishes
Sometimes you do not want a notification pop-up, just a sound: a little chime when Claude Code finishes so you can keep your eyes on something else. That is a two-minute setup with the Stop hook.
If hooks are new to you, skim the complete guide to Claude Code hooks first; otherwise, here are the one-liners per platform. All of them go in a settings.json, either ~/.claude/settings.json for every project or .claude/settings.json for one repository.
macOS
Play a built-in system sound with afplay:
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "afplay /System/Library/Sounds/Glass.aiff" }] }
]
}
}
Swap Glass for Ping, Hero, Submarine, or anything in /System/Library/Sounds/. Run ls /System/Library/Sounds/ to hear your options, and afplay -v 0.4 … if the default is too loud for an open office.
Linux
Use paplay (PulseAudio/PipeWire) with any sound file:
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "paplay /usr/share/sounds/freedesktop/stereo/complete.oga" }] }
]
}
}
The freedesktop theme ships complete.oga, bell.oga, message.oga and a few more, so you have a small palette without downloading anything. No PulseAudio? aplay handles WAV files, and ffplay -nodisp -autoexit plays anything at all. No sound daemon at all? The terminal bell always works: printf '\\a'.
Windows
PowerShell can beep or play a system sound:
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "powershell -c (New-Object Media.SoundPlayer 'C:\\Windows\\Media\\notify.wav').PlaySync()" }] }
]
}
}
C:\Windows\Media\ holds the rest of the system sounds. If you run Claude Code inside WSL, call the Windows binary from the Linux side with powershell.exe -c …, since a Linux audio player has no speaker to reach without WSLg.
Different sounds for different events
Because each event is a separate hook, you can give "needs you" and "done" distinct sounds: an urgent one when a session is blocked on you (Notification), a gentle one when it finishes (Stop):
{
"hooks": {
"Notification": [
{ "hooks": [{ "type": "command", "command": "afplay /System/Library/Sounds/Ping.aiff" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "afplay /System/Library/Sounds/Glass.aiff" }] }
]
}
}
Two sounds is about the limit of what you can tell apart without thinking. Past that, you are learning a language rather than getting a cue.
A different sound per project
If you run several repositories, put the sound in the project settings file rather than your user one, and each gets its own chime. .claude/settings.json in the API repo can play Submarine.aiff while the frontend plays Hero.aiff, and the same hook block layers cleanly on top of whatever you have in ~/.claude/settings.json.
Do not chime on every little turn
A sound after a three second turn is noise. Gate it on how long the turn actually took by stamping the start on UserPromptSubmit and checking it on Stop:
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "date +%s > /tmp/claude-start" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "[ $(( $(date +%s) - $(cat /tmp/claude-start) )) -gt 45 ] && afplay /System/Library/Sounds/Glass.aiff" }] }
]
}
}
Now only the runs long enough for you to have wandered off make a sound. If you run several sessions at once, write the stamp to a per-session file using the session_id from the payload instead of a shared /tmp path.
Pick the right event, not all of them
Stop is the event you want for "it is done". Two neighbours look tempting and are traps. SubagentStop fires once per subagent, so a single fan-out can chime eight times in twenty seconds. SessionEnd fires when you close the session, which you already know about because you did it. If you only wire one hook, wire Stop; if you wire two, add Notification, because being blocked is the thing genuinely worth interrupting you for.
Let your terminal do the work
If you live in tmux or a terminal with triggers, the bell is more useful than a sound file, because your multiplexer can act on it. Ring it from the hook:
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "printf '\\a' > /dev/tty" }] }
]
}
}
Then let tmux flag the window it came from, so the sound tells you where as well as what:
# ~/.tmux.conf
set -g monitor-bell on
set -g visual-bell both
Now the status bar highlights the window whose session finished, which is the piece a plain chime never gives you.
Why nothing plays
Three usual suspects. Hooks are snapshotted when a session starts, so a hook added mid-session does nothing until you restart Claude Code or review it from /hooks. Hooks also run without your interactive shell setup, so use absolute paths to both the player and the sound file. And on a remote or SSH session there is simply no audio device to reach: run claude --debug and you will see the hook fire and the player fail.
When a sound is not enough
A sound tells you something happened; it does not tell you which session, or what. With one agent that is fine. With several running at once, a chime just sends you hunting through terminals to find the one that finished, or the one still waiting on you. Add SubagentStop to the mix and a busy run can chime a dozen times without ever being done.
Blooby uses the very same events, but shows them: each Claude Code session gets its own mascot that celebrates when it finishes and looks up when it needs you, so you glance instead of guess. No sound files, no config to maintain.
See it for yourself
download Get Blooby free