I’ve been using Claude Code a lot lately, and one thing that kept bugging me was missing when it needed my input. I’d be in another tab, another window, deep in other projects, and Claude’s just sitting there waiting for me to approve a tool call or answer a question.

Two things I wanted out of this:

  • The window where Claude Code is waiting pops to the front automatically
  • A sound fires so I hear it even if I’m looking elsewhere

The whole thing ended up being a Claude Code hook, one 40 line shell script, and a Star Wars fanfare generated from pure Python. No installs. No app. No brew. No npm. Just macOS, osascript, and afplay.

The Hook

Claude Code has a hooks system that runs shell commands on lifecycle events. The Notification hook fires whenever Claude needs your attention. Tool approvals, questions, task completion. Add this to your global settings at ~/.claude/settings.json:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/notify.sh"
          }
        ]
      }
    ]
  }
}

That is the only change you make to settings.json. Everything else lives in the shell script.

The Script

Save this as ~/.claude/notify.sh and chmod +x it:

#!/bin/bash
# Claude Code Notification hook: bring the host app window to the front and play the fanfare.
# Walks the process tree up from $PPID to find the GUI host app (Zed, Terminal, iTerm, etc.).

pid=$PPID
host_app_path=""
while [ -n "$pid" ] && [ "$pid" != "1" ] && [ "$pid" != "0" ]; do
    comm=$(ps -o comm= -p "$pid" 2>/dev/null)
    if [[ "$comm" == *.app/* ]]; then
        host_app_path="${comm%.app/*}.app"
        break
    fi
    pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
done

app_name="Zed"
if [ -n "$host_app_path" ]; then
    app_name=$(basename "$host_app_path" .app)
fi

# Terminal.app gets per-window targeting via TTY matching. Everything else gets app-level focus.
if [ "$app_name" = "Terminal" ]; then
    target_tty=$(tty 2>/dev/null)
    [ "$target_tty" = "not a tty" ] && target_tty=""
    if [ -n "$target_tty" ]; then
        osascript <<OSASCRIPT
tell application "Terminal"
    activate
    repeat with w in windows
        repeat with t in tabs of w
            if tty of t is "$target_tty" then
                set selected tab of w to t
                set index of w to 1
                return
            end if
        end repeat
    end repeat
end tell
OSASCRIPT
    else
        osascript -e 'tell application "Terminal" to activate'
    fi
else
    osascript -e "tell application \"$app_name\" to activate"
fi

afplay ~/.claude/star_wars.wav

Three things are doing the work here:

  • The PPID walk. Starting from the parent process of this script, it walks up the process tree until it hits something running inside a .app bundle. That is your host app, whether it is Zed, Terminal, iTerm, Warp, or anything else. No hardcoded assumptions.
  • osascript ... activate. Brings the host app to the foreground. This is how the window pops up. For most apps that is as deep as macOS lets you go.
  • Terminal TTY targeting. Terminal.app is one of the few terminal emulators Apple ships with a full AppleScript dictionary that exposes tty on each tab. If the hook is running under Terminal.app, the script captures the current tty, then loops every window and tab, matches on tty, and raises the exact window Claude Code is attached to. If you have four Terminal windows open and Claude is in window three, window three comes to the front, not window one.

Zed is not AppleScript scriptable, so you get app level focus and that is it. iTerm2, Ghostty, Kitty, Warp, and friends each have their own story. If you want yours added, drop a comment below and I will work it in.

But Why Use a Default Sound When You Can Have the Star Wars Theme?

Here’s where it got fun. Instead of a boring system ping, I put together the Star Wars main title fanfare, the iconic “da da da DAAAA da,” as a WAV file using nothing but Python’s standard library. No ffmpeg, no sox, no downloads. Just math.sin() and the wave module.

Here’s the script that generates it:

import wave, struct, math

SAMPLE_RATE = 44100
AMPLITUDE = 0.6

def tone(freq, duration):
    n = int(SAMPLE_RATE * duration)
    fade = int(SAMPLE_RATE * 0.01)
    samples = []
    for i in range(n):
        env = 1.0
        if i < fade:
            env = i / fade
        elif i > n - fade:
            env = (n - i) / fade
        val = (
            math.sin(2 * math.pi * freq * i / SAMPLE_RATE) * 0.7
            + math.sin(2 * math.pi * freq * 2 * i / SAMPLE_RATE) * 0.2
            + math.sin(2 * math.pi * freq * 3 * i / SAMPLE_RATE) * 0.1
        )
        samples.append(int(val * env * AMPLITUDE * 32767))
    return samples

BPM = 108
q = 60 / BPM
de = q * 0.75
s = q * 0.25
h = q * 2
triplet = q / 3

# First 19 notes of the Star Wars main title fanfare, ~9 seconds
notes = [
    (233.08, triplet), (233.08, triplet), (233.08, triplet),
    (349.23, h), (523.25, h),
    (466.16, triplet), (440.00, triplet), (392.00, triplet),
    (698.46, h), (523.25, q),
    (466.16, triplet), (440.00, triplet), (392.00, triplet),
    (698.46, h), (523.25, q),
    (466.16, triplet), (440.00, triplet), (466.16, triplet),
    (392.00, h),
]

samples = []
gap = [0] * int(SAMPLE_RATE * 0.02)
for freq, dur in notes:
    samples.extend(tone(freq, dur - 0.02))
    samples.extend(gap)

with wave.open("star_wars.wav", "w") as w:
    w.setnchannels(1)
    w.setsampwidth(2)
    w.setframerate(SAMPLE_RATE)
    w.writeframes(struct.pack(f"<{len(samples)}h", *samples))

Run it, drop the WAV in ~/.claude/, and you’re done.

How It Works

The script builds audio from scratch:

  • Each note is a sine wave at the correct frequency (e.g., Bb3 = 233.08 Hz, F4 = 349.23 Hz)
  • Harmonics are layered on top (2nd and 3rd overtones) to make it sound richer than a plain beep
  • A quick fade in/out on each note prevents clicking artifacts
  • The notes follow the actual Star Wars main title melody in Bb major at 108 BPM
  • It all gets packed into a standard WAV file using Python’s built-in wave and struct modules

The Result

Now every time Claude Code needs my attention, the window it is running in pops to the front and the Star Wars opening fanfare plays. No guessing which terminal. No clicking anything. No brew installs, no npm packages, no dependencies. Just Python, bash, osascript, and afplay.

It started as “how do I get a notification sound?” and ended as a custom composed Star Wars theme generated from sine waves plus a 40 line shell script that routes focus back into the right window.

Do Not Disturb and the Lock Screen

A couple of behaviors worth knowing about. If the Mac is asleep, nothing fires at all because afplay needs the CPU awake. When you wake the Mac, nothing is queued. If the Mac is locked but not asleep, the fanfare plays to completion but osascript ... activate cannot raise windows through the lock screen. And Do Not Disturb or Focus modes do not affect this setup at all, because there is no notification banner involved. The fanfare plays and osascript activates the app the moment you unlock.

Quick Reference

What Where
Claude Code settings ~/.claude/settings.json
Hook script ~/.claude/notify.sh
Sound file ~/.claude/star_wars.wav
Audio generator ~/.claude/star_wars.py
Test the sound afplay ~/.claude/star_wars.wav
Test the full flow ~/.claude/notify.sh

Want a different tune? Just change the frequencies and durations in the notes array of the Python generator. The whole melody is a list of (frequency_hz, duration_seconds) tuples. Go nuts.

Star Wars is a trademark of Lucasfilm Ltd. This post is for educational purposes and is not affiliated with or endorsed by Lucasfilm or Disney.