Claude Code sound when done: play a sound when it finishes or needs you
Updated 24 October 2026
A sound is the simplest way to stop checking the terminal every thirty seconds. Claude Code doesn’t play one by itself, but two hooks cover it: Stop when Claude finishes a reply, and PermissionRequest when it’s waiting for your approval. Here’s the setup for macOS, Linux, Windows and WSL, and what to check when it stays quiet.
The short answer
| System | Command for your hook |
|---|---|
| macOS | afplay /System/Library/Sounds/Glass.aiff |
| Linux | paplay /usr/share/sounds/freedesktop/stereo/complete.oga |
| Windows and WSL | powershell.exe with Media.SoundPlayer and PlaySync(), below |
| Any terminal, including over SSH | The terminal bell, below |
Put the command in a Stop hook for “finished” and a PermissionRequest hook for “needs you”, in ~/.claude/settings.json so it works in every project.
1. Which event to hook: when done, or when input is needed
| You want a sound when… | Event | Timing |
|---|---|---|
| Claude finished replying | Stop | Immediately, after every reply |
| Claude needs permission | PermissionRequest | Immediately, when the prompt appears |
| Claude asks you a multiple-choice question | PreToolUse on AskUserQuestion | Immediately |
| You walked away and Claude is waiting | Notification with idle_prompt | About 60 seconds after it finishes, if you haven’t typed |
| The turn died on an API error | StopFailure | Immediately, instead of Stop |
Two things to know before you start. Stop runs whenever Claude finishes responding, including after a one-line answer, and it doesn’t run when you interrupt with Esc. And the permission_prompt notification many guides suggest only fires after about six seconds without typing, so it’s unreliable while you’re at the keyboard. PermissionRequest has no delay.
2. macOS
afplay ships with macOS and plays any audio file. Try a sound in your terminal first:
afplay /System/Library/Sounds/Glass.aiff
The built-in sounds in /System/Library/Sounds/ are Basso, Blow, Bottle, Frog, Funk, Glass, Hero, Morse, Ping, Pop, Purr, Sosumi, Submarine and Tink, each an .aiff file. Then add both hooks:
~/.claude/settings.json{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff",
"async": true
}
]
}
],
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Funk.aiff",
"async": true
}
]
}
]
}
}
"async": true runs the sound in the background. Without it, Claude Code waits for the sound to finish, so the permission prompt appears a second later than it should. Add -v 0.5 after afplay to play it at half volume. If you already have a "hooks" key, add Stop and PermissionRequest inside it.
A PermissionRequest hook that prints nothing doesn’t approve or deny anything. The prompt still waits for you.
3. Linux
Most desktops run PipeWire or PulseAudio, and paplay works with both. The sounds come from the freedesktop sound theme:
sudo apt-get install pulseaudio-utils sound-theme-freedesktop
paplay /usr/share/sounds/freedesktop/stereo/complete.oga
~/.claude/settings.json{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "paplay /usr/share/sounds/freedesktop/stereo/complete.oga",
"async": true
}
]
}
],
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "paplay /usr/share/sounds/freedesktop/stereo/message-new-instant.oga",
"async": true
}
]
}
]
}
}
Other options, if you prefer:
pw-playtakes the same file on PipeWire systems.canberra-gtk-play --id=completeplays the “complete” sound from your current desktop sound theme instead of a fixed file. On Debian and Ubuntu it’s in thegnome-session-canberrapackage.aplayworks on bare ALSA, but only with.wavfiles.
Other files in the same folder include bell.oga, dialog-warning.oga and dialog-information.oga. Over SSH, the sound plays on the remote machine, which usually has no speakers. Use the terminal bell there.
4. Windows
Windows has no one-word sound command, but PowerShell can play any .wav file through .NET’s SoundPlayer. Test it in a terminal:
powershell.exe -NoProfile -Command "(New-Object Media.SoundPlayer 'C:\Windows\Media\tada.wav').PlaySync()"
Use PlaySync(), not Play(). Play() returns straight away, PowerShell exits, and the sound is cut off before it starts. That’s the most common reason a Windows sound hook stays silent.
In the hook, give the command as an executable plus args. Claude Code then starts powershell.exe directly, whether or not Git Bash is installed, and there’s no shell quoting to get wrong:
%USERPROFILE%\.claude\settings.json{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "powershell.exe",
"args": ["-NoProfile", "-Command", "(New-Object Media.SoundPlayer 'C:\\Windows\\Media\\tada.wav').PlaySync()"],
"async": true
}
]
}
],
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "powershell.exe",
"args": ["-NoProfile", "-Command", "(New-Object Media.SoundPlayer 'C:\\Windows\\Media\\notify.wav').PlaySync()"],
"async": true
}
]
}
]
}
}
Backslashes are doubled because it’s JSON. Run dir C:\Windows\Media to see which sounds your Windows version has. SoundPlayer only plays .wav, so convert anything else first.
5. WSL
Claude Code inside WSL runs as a Linux program, but it can still start Windows programs. The Windows config above works unchanged in ~/.claude/settings.json inside your distro, because WSL finds powershell.exe on your Windows PATH. Check that first:
powershell.exe -NoProfile -Command "(New-Object Media.SoundPlayer 'C:\Windows\Media\tada.wav').PlaySync()"
If you get command not found, Windows interop or the Windows path is turned off in /etc/wsl.conf. Look for appendWindowsPath = false under [interop], remove it, and run wsl --shutdown from Windows. Or use the full path, /mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe, as the hook’s command. Keep the sound file path in Windows form (C:\…), since PowerShell is the one opening it.
6. A different sound for each event
Distinct sounds tell you what happened without looking: done, needs approval, has a question, or broke. This macOS config uses four. Swap the commands for the Linux or Windows ones above:
~/.claude/settings.json{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "afplay /System/Library/Sounds/Glass.aiff", "async": true }
]
}
],
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "afplay /System/Library/Sounds/Funk.aiff", "async": true }
]
}
],
"PreToolUse": [
{
"matcher": "AskUserQuestion",
"hooks": [
{ "type": "command", "command": "afplay /System/Library/Sounds/Ping.aiff", "async": true }
]
}
],
"StopFailure": [
{
"hooks": [
{ "type": "command", "command": "afplay /System/Library/Sounds/Basso.aiff", "async": true }
]
}
]
}
}
Leave Notification out of this, or limit it to idle_prompt. A Notification hook with no matcher also fires for permission_prompt, so you’d hear a second sound six seconds after the first.
To play the “done” sound only when nothing is still running in the background, such as a background shell or subagent, check the background_tasks field that Stop receives:
~/.claude/settings.json{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "if jq -e '(.background_tasks // []) | length == 0' >/dev/null; then afplay /System/Library/Sounds/Glass.aiff; fi",
"async": true
}
]
}
]
}
}
That needs jq (brew install jq or sudo apt-get install jq). The if keeps the hook’s exit code at 0 when it skips the sound, so you don’t get a hook error notice.
7. A beep when done: the terminal bell
The bell needs no sound files and works over SSH, because it’s just a character sent to your own terminal. For notifications, one setting turns it on:
~/.claude/settings.json{
"preferredNotifChannel": "terminal_bell"
}
That rings on Claude Code’s notifications, which follow the same timing as above: about six seconds after a permission prompt if you’re not typing, and about 60 seconds after it finishes. For a bell the moment Claude finishes, a hook can’t just print \a: hooks have no terminal, and writing to /dev/tty fails. Return the bell in terminalSequence and Claude Code writes it for you:
~/.claude/settings.json{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "printf '%s\\n' '{\"terminalSequence\":\"\\u0007\"}'"
}
]
}
]
}
}
Leave out async here: an async hook’s output only reaches Claude Code on the next turn. Whether a bell makes a noise, flashes or bounces the Dock icon is up to your terminal. In Apple’s Terminal, Claude Code’s first-run setup (/terminal-setup) turns the audible bell off; turn it back on under Settings → Profiles → Advanced → Audible bell.
8. Spoken alerts
With several sessions open, a voice that says which project finished beats a chime. Claude Code sets CLAUDE_PROJECT_DIR for every hook, so the folder name is easy to get:
- macOS:
sayis built in. List voices withsay -v '?'. - Linux:
spd-sayfrom Speech Dispatcher, orespeak-ng. - Windows: PowerShell’s
System.Speech, shown below.
~/.claude/settings.json (macOS){
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "say \"$(basename \"$CLAUDE_PROJECT_DIR\") is done\"",
"async": true
}
]
}
],
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "say \"$(basename \"$CLAUDE_PROJECT_DIR\") needs permission\"",
"async": true
}
]
}
]
}
}
On Linux, replace say with spd-say. On Windows, use powershell.exe with these args:
["-NoProfile", "-Command", "Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak('Claude is done')"]
9. No sounds from subagents or scripts
- Stop is only the main agent. Subagents fire
SubagentStopinstead, so a Stop sound won’t go off every time Claude delegates a search. - Don’t hook
SubagentStopfor sounds. It fires for every subagent, and also when some of Claude Code’s own background helpers finish, such as prompt suggestions and/btwside questions. You’d hear sounds you can’t explain. - Subagent files convert Stop. A
Stophook written in a subagent’s frontmatter becomesSubagentStop. If you hear sounds you didn’t set up, type/hooks: it shows every hook and where it came from, including plugins. - Scripts read the same settings. If you run
claude -pin scripts or CI on the same machine, your Stop hook runs there too. Async hooks are killed when a-prun exits, so you may hear a clipped sound or nothing. To run a script with no hooks at all, pass--settings '{"disableAllHooks": true}'.
10. Troubleshooting
- No sound at all. Run the command in a normal terminal first. If that’s silent, it’s your volume, output device or a missing file, not Claude Code. If it works there, type
/hooksand check the hook is listed under the right event. - A
hook errornotice with exit code 127, ornot found. The player isn’t installed or isn’t on thePATHClaude Code sees. Install it, or use its full path, such as/usr/bin/paplay. - The sound only plays sometimes when Claude needs permission. You’re using
Notificationwithpermission_prompt, which waits for six seconds without typing. Switch toPermissionRequest. - Two sounds for one prompt. Both
PermissionRequestand aNotificationhook are firing, or different sound hooks sit in your user and project settings. Identical handlers in two files run once; different ones both run. - A sound after every short answer. That’s
Stopworking as designed. UseNotificationwithidle_promptif you only want it when you’ve stepped away. - The prompt appears after the sound. Add
"async": true. - Silent on Windows. Use
PlaySync(), a.wavfile, and doubled backslashes in JSON. - No sound when you press Esc. Expected:
Stopdoesn’t run when you interrupt. API errors fireStopFailureinstead. - Nothing in a new project folder. Hooks don’t run until you accept Claude Code’s trust dialog for that folder.
For anything else, start Claude Code with claude --debug-file /tmp/claude.log and look for your hook in the log. Claude Code hooks: a practical guide covers debugging in detail.
More than a sound: answer from your browser
A sound tells you to go back to the terminal. Agent Pets brings the prompt to you instead. Each Claude Code, Codex, OpenCode and omp session becomes a pet walking along the bottom of every page in Chrome. When one needs permission, its pet bounces; click it and answer Allow, Always or Deny. A capsule over each pet says Thinking, Your Turn or Done, and in another app a desktop notification names the pet that needs you, with Allow and Deny on it.
npx getagentpets init adds the hooks for you, next to any sound hooks you already have. It runs entirely on your computer.
Get Agent Pets — was $25, now $6Get Agent Pets — $25
Questions
How do I make Claude Code play a sound when it finishes?
Add a Stop hook to ~/.claude/settings.json that runs a sound command: afplay /System/Library/Sounds/Glass.aiff on macOS, or paplay /usr/share/sounds/freedesktop/stereo/complete.oga on Linux. Type /hooks in a session to check it loaded.
How do I get a sound when Claude Code needs permission?
Use a PermissionRequest hook. It runs as soon as Claude asks. A Notification hook with the permission_prompt matcher waits until you haven’t typed for about six seconds.
Why does the sound play after every reply?
Stop runs every time Claude finishes responding, not only when a whole task is done. If you only want a sound once you’ve walked away, use a Notification hook with the idle_prompt matcher instead. It fires about 60 seconds after Claude finishes, and only if you haven’t typed since.
How do I play a sound from Claude Code on Windows or WSL?
Run powershell.exe with System.Media.SoundPlayer and call PlaySync() on a .wav file from C:\Windows\Media. Play() returns at once and PowerShell exits before the sound starts. From WSL, the same command works through Windows interop.
How do I make Claude Code beep when done?
Have a Stop hook return the bell character in terminalSequence, as in the terminal bell section above. Claude Code writes it to your terminal the moment Claude finishes, even over SSH. Whether the bell beeps or flashes depends on your terminal’s settings.
Can I change the Claude Code notification sound?
Claude Code doesn’t play a sound of its own, so there’s no built-in setting for it. With a hook, the sound is whatever file you pass to afplay, paplay or SoundPlayer, so use a different file for each event if you like.
Sources: Claude Code’s documentation on the hooks reference, automating actions with hooks and terminal configuration; Microsoft’s SoundPlayer.PlaySync reference and WSL docs on running Windows tools from Linux and wsl.conf; the sound-theme-freedesktop file list and the canberra-gtk-play man page.