Skip to content

Instantly share code, notes, and snippets.

@FrancescoBorzi
Created June 25, 2026 14:32
Show Gist options
  • Select an option

  • Save FrancescoBorzi/32af326f44fbc48b6691b1e643a31591 to your computer and use it in GitHub Desktop.

Select an option

Save FrancescoBorzi/32af326f44fbc48b6691b1e643a31591 to your computer and use it in GitHub Desktop.
Claude Code Stop hook: sound on turn completion, but only when the terminal window is unfocused (macOS)

Claude Code: play a sound on turn completion, only when unfocused (macOS)

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.

Why

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.

Install

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.

How it works (the same command, expanded)

# 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.aiff

The 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, ...).

Caveats

  • macOS only. lsappinfo (frontmost app) and afplay (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.

Porting to Linux

Two pieces need swapping; the ancestry walk itself is portable.

  • Sound: replace afplay with paplay, aplay, pw-play, or canberra-gtk-play -i message.
  • Focus, X11: replace the lsappinfo line with xdotool getactivewindow getwindowpid (or xprop -root _NET_ACTIVE_WINDOW, then read _NET_WM_PID), and feed that PID in as fp.
  • 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment