A Claude Code Stop hook that plays a sound every time a turn finishes, but stays silent when the window hosting the session is focused. So you only hear it when you've tabbed away.
It works whether Claude Code runs in Terminal, iTerm2, Ghostty, or an editor's integrated terminal (Zed, VS Code), because the focus check walks the process tree rather than matching app names.
Claude Code's built-in terminal bell only fires when it needs your attention (a permission prompt or waiting on input), not on plain quick completions. If you want an audible "done" on every turn, a
Stop hook with afplay is reliable and independent of your terminal's bell settings.
Merge this into ~/.claude/settings.json. If the file already has other keys, just add the "hooks" key alongside them.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "fp=$(lsappinfo info -only pid \"$(lsappinfo front)\" 2>/dev/null | sed 's/[^0-9]//g'); p=$$; while [ -n \"$p\" ] && [ \"$p\" -gt 1 ]; do [ \"$p\" = \"$fp\" ] && exit 0; p=$(ps -o
ppid= -p \"$p\" 2>/dev/null | tr -d ' '); done; afplay /System/Library/Sounds/Glass.aiff"
}
]
}
]
}
}Reload with /hooks in the session, or restart it, for the hook to take effect.
# Frontmost GUI app's PID. lsappinfo is built into macOS and needs no
# Automation/Accessibility permission, unlike AppleScript System Events.
fp=$(lsappinfo info -only pid "$(lsappinfo front)" 2>/dev/null | sed 's/[^0-9]//g')
# Walk this shell's ancestry. If the frontmost app is one of our ancestors,
# the window hosting Claude Code is focused, so stay silent.
p=$$
while [ -n "$p" ] && [ "$p" -gt 1 ]; do
[ "$p" = "$fp" ] && exit 0
p=$(ps -o ppid= -p "$p" 2>/dev/null | tr -d ' ')
done
# Otherwise the window is in the background, so play a sound.
afplay /System/Library/Sounds/Glass.aiffThe ancestry walk is what makes it host-agnostic: the Claude Code shell is always a descendant of its host GUI app, even through helper processes like VS Code's pty-host. Matching the frontmost PID against the ancestors is more robust than matching an app name.
Swap the sound for any file in /System/Library/Sounds/ (Glass, Ping, Hero, Submarine, Funk, ...).
- macOS only.
lsappinfo(frontmost app) andafplay(playback) are macOS-specific. - tmux / screen: the shell's ancestor is the multiplexer server (parented to
launchd), not the terminal app, so the focus check can't match and it always plays. - SSH / remote: there's no local host app in the process tree, so it always plays.
Two pieces need swapping; the ancestry walk itself is portable.
- Sound: replace
afplaywithpaplay,aplay,pw-play, orcanberra-gtk-play -i message. - Focus, X11: replace the
lsappinfoline withxdotool getactivewindow getwindowpid(orxprop -root _NET_ACTIVE_WINDOW, then read_NET_WM_PID), and feed that PID in asfp. - Focus, Wayland: there's no generic way to get the focused window's PID. Either drop the focus gate and always play, or use compositor-specific tools (
swaymsg,hyprctl).