Like Elena waiting for Damon to become "good," I was staring at my Claude Code terminal, waiting for it to finish my request.
One day, I was so fed up with waiting that I asked myself, "What could make this better?"
The first thing that came to mind was...
C A T S. 🐈
But how?
Then my inner Claude ran /research
inside my brain, and that's how this idea was born.
I needed to fulfill two requirements:
🐈⬛ notify me when Claude had finished its work;
🐈 notify me when it needed my permission and was blocked waiting for it.
But I also wanted to make my workflow less stressful 🥹.
Here's how to add sound hooks to Claude Code using nothing more than a couple of shell scripts and two .mp3
files.
Two hook events in ~/.claude/settings.json
trigger shell scripts:
| Event | Script | When it fires |
|---|---|---|
Stop |
||
play-success.sh |
||
| Claude finishes its turn (response complete) | ||
PermissionRequest |
||
play-permission.sh |
||
| Claude is waiting for your approval |
Both run with "async": true
, so the sound plays in the background without delaying Claude's response or the permission prompt.
~/.claude/
settings.json
hooks/
_play.sh <- shared helper (source this, don't call directly)
play-success.sh
play-permission.sh
sounds/
success.mp3
permission.mp3
.mp3
files
Place sound files in ~/.claude/hooks/sounds/
. Missing files are silently skipped — no errors.
💡 Tip: I use two different meows: a happy, purring one when Claude finishes successfully, and a slightly worried one when it's waiting for my approval.
_play.sh
(shared helper)
#!/usr/bin/env bash
SOUNDS_DIR="$HOME/.claude/hooks/sounds"
play_sound() {
local name="$1"
local file="$SOUNDS_DIR/${name}.mp3"
[ -f "$file" ] || return 0
if command -v afplay &>/dev/null; then
afplay "$file" &>/dev/null &
elif command -v powershell.exe &>/dev/null; then
local win_path
win_path=$(echo "$file" | sed 's|^/\([a-zA-Z]\)/|\1:/|')
win_path="${win_path^}"
powershell.exe -NoProfile -NonInteractive -c "
Add-Type -AssemblyName PresentationCore
\$m = New-Object System.Windows.Media.MediaPlayer
\$m.Open([uri]'file:///$win_path')
\$m.Play()
Start-Sleep -Milliseconds 500
while (\$m.NaturalDuration -eq [System.Windows.Duration]::Automatic) {
Start-Sleep -Milliseconds 100
}
Start-Sleep -Milliseconds \$m.NaturalDuration.TimeSpan.TotalMilliseconds
\$m.Stop()
" &>/dev/null &
fi
}
** play-success.sh** — fires when Claude finishes a turn:
#!/usr/bin/env bash
source "$HOME/.claude/hooks/_play.sh"
play_sound "success"
** play-permission.sh** — fires when Claude is waiting for your approval:
#!/usr/bin/env bash
source "$HOME/.claude/hooks/_play.sh"
play_sound "permission"
~/.claude/settings.json
{
"hooks": {
"Stop": [
{
"matcher": ".*",
"hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/play-success.sh", "async": true }]
}
],
"PermissionRequest": [
{
"matcher": ".*",
"hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/play-permission.sh", "async": true }]
}
]
}
}
Run scripts directly to verify audio works before wiring into Claude:
bash ~/.claude/hooks/play-success.sh
bash ~/.claude/hooks/play-permission.sh
Then trigger each event from Claude: finish any prompt to hear success.mp3
, or trigger a permission request (e.g. ask Claude to run a shell command in auto-approve mode) to hear permission.mp3
.
As a result, I can freely switch between tabs while Claude is running commands without worrying that the process has d because it's waiting for my attention.
** Unlike Elena, I don't have to spend seasons waiting anymore**💁♀️