Here’s how I got Claude Code to play a completion sound when it finisehes responding, and an approval sound when it needs my sign-off to run a command.
I set it up to play a random sound from a folder of audio clips to keep it interesting, but you can use a single sound if you prefer.
All we need is a single global config file and two small scripts, and it works automatically in every project.
No more waiting around. I can minimize the terminal, switch to another app, and know Claude will call me when it’s done.
How Claude Code Hooks Work
Claude Code has a hook system that lets you run shell commands in response to lifecycle events. You define hooks in ~/.claude/settings.json under a hooks key. Each hook entry specifies an event type and a command to run.
The two events we care about:
Stop: fires when Claude finishes generating a responsePermissionRequest: fires when Claude needs user approval before running a tool
These are global hooks, so they apply to every project automatically. The full list of available hook events is in the Claude Code hooks reference.
Step 1: Create the Sounds Directories
Create two folders inside .claude/sounds/ to keep the pools separate:
Windows:
C:\Users\YOUR_USERNAME\.claude\sounds\completion\
C:\Users\YOUR_USERNAME\.claude\sounds\approval\
macOS / Linux:
~/.claude/sounds/completion/
~/.claude/sounds/approval/
Add MP3 or WAV files to each. I create voice clips with ElevenLabs. Keep them short so they don’t overstay their welcome.
completion/: plays when Claude finishes a response. Mine has short clips like “Another one” by DJ Khalid and a few French phrases because my French really needs the practice.approval/: plays when Claude needs your sign-off. My favorite show is Burn Notice so one of my clips here is the “Someone needs your help Michael” line from the intro.
Step 2: Create the Scripts
You need two scripts: one that picks randomly from completion/, one that picks randomly from approval/.
Windows
Create these two PowerShell scripts in your sounds folder.
play.ps1 (random completion sound):
Add-Type -AssemblyName presentationCore
$soundDir = 'C:/Users/YOUR_USERNAME/.claude/sounds/completion'
$sounds = @(Get-ChildItem -Path "$soundDir/*.mp3") + @(Get-ChildItem -Path "$soundDir/*.wav")
$pick = $sounds | Get-Random
$mp = New-Object System.Windows.Media.MediaPlayer
$mp.Open([uri]$pick.FullName)
$mp.Play()
Start-Sleep 4
play-approval.ps1 (random approval sound):
Add-Type -AssemblyName presentationCore
$soundDir = 'C:/Users/YOUR_USERNAME/.claude/sounds/approval'
$sounds = @(Get-ChildItem -Path "$soundDir/*.mp3") + @(Get-ChildItem -Path "$soundDir/*.wav")
$pick = $sounds | Get-Random
$mp = New-Object System.Windows.Media.MediaPlayer
$mp.Open([uri]$pick.FullName)
$mp.Play()
Start-Sleep 4
Replace YOUR_USERNAME with your actual Windows username in both files.
The Start-Sleep 4 keeps the PowerShell process alive long enough for the audio to finish before the process exits. Without it, the sound gets cut off immediately. The scripts use System.Windows.Media.MediaPlayer, which is built into Windows and supports MP3 and WAV without any extra dependencies.
macOS
Create these two shell scripts in your sounds folder.
play.sh (random completion sound):
#!/bin/bash
SOUND_DIR="$HOME/.claude/sounds/completion"
FILES=($(ls "$SOUND_DIR"/*.mp3 "$SOUND_DIR"/*.wav 2>/dev/null))
PICK="${FILES[$RANDOM % ${#FILES[@]}]}"
afplay "$PICK"
play-approval.sh (random approval sound):
#!/bin/bash
SOUND_DIR="$HOME/.claude/sounds/approval"
FILES=($(ls "$SOUND_DIR"/*.mp3 "$SOUND_DIR"/*.wav 2>/dev/null))
PICK="${FILES[$RANDOM % ${#FILES[@]}]}"
afplay "$PICK"
afplay is built into macOS and handles both MP3 and WAV. After creating the files, make them executable:
chmod +x ~/.claude/sounds/play.sh ~/.claude/sounds/play-approval.sh
On Linux, replace afplay with aplay for WAV files, or install mpg123 for MP3 support.
Step 3: Configure the Hooks
Open (or create) ~/.claude/settings.json and add the hooks section. You also need to pre-approve the script commands in permissions.allow so Claude can run them automatically without prompting you each time.
Windows
{
"permissions": {
"allow": [
"Bash(powershell -WindowStyle Hidden -File \"C:/Users/YOUR_USERNAME/.claude/sounds/play.ps1\")",
"Bash(powershell -WindowStyle Hidden -File \"C:/Users/YOUR_USERNAME/.claude/sounds/play-approval.ps1\")"
]
},
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "powershell -WindowStyle Hidden -File \"C:/Users/YOUR_USERNAME/.claude/sounds/play.ps1\""
}
]
}
],
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "powershell -WindowStyle Hidden -File \"C:/Users/YOUR_USERNAME/.claude/sounds/play-approval.ps1\""
}
]
}
]
}
}
The -WindowStyle Hidden flag runs PowerShell in the background without opening a visible terminal window.
macOS / Linux
{
"permissions": {
"allow": [
"Bash(bash \"/Users/YOUR_USERNAME/.claude/sounds/play.sh\")",
"Bash(bash \"/Users/YOUR_USERNAME/.claude/sounds/play-approval.sh\")"
]
},
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash \"/Users/YOUR_USERNAME/.claude/sounds/play.sh\""
}
]
}
],
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash \"/Users/YOUR_USERNAME/.claude/sounds/play-approval.sh\""
}
]
}
]
}
}
Replace YOUR_USERNAME with your macOS username in both the permissions.allow entries and the hook commands.
The matcher field can filter which tool calls trigger the hook. An empty string matches everything, which is what we want here.
If your settings.json already has other content (permissions, model preferences, etc.), merge these sections in rather than replacing the whole file.
That’s it: Two folders, two scripts, a few lines of JSON. Enjoy.