Konabos

Stop Babysitting Claude Code: A Five Minute Sound Notification Setup

Akshay Sura - Partner

13 May 2026

Share on social media

You kick off a Claude Code task. You alt-tab to Slack to answer a quick message. Twenty minutes later you come back and Claude has been sitting there for the last eighteen of those, waiting for you to approve a permission prompt.

If you have run more than a couple of long sessions in Claude Code, you know the pattern. The CLI is silent by design. There is no system bell, no popup, no audio cue. You either babysit the terminal or you lose time.

This post walks through the simplest fix: native OS sound hooks. Two sounds, one config file, no dependencies. Five minutes to install. We will also cover why we picked this approach over the community plugins floating around on GitHub, and why we dropped a fancier version that escalated the reminder over time.

What we built

Two hooks that play a sound when Claude Code wants something from you:

  • A Notification sound when Claude is waiting for input or permission
  • A Stop sound when a task finishes

Different tones so you can tell them apart from the next room. The whole thing is one JSON block in ~/.claude/settings.json and uses audio players that ship with the OS. Nothing to install, nothing to maintain.

We packaged it as a one-shot installer per platform so the team can drop it into any new dev environment without copy-pasting JSON snippets.

Why hooks at all

Claude Code has a hook system. It fires shell commands at specific lifecycle events: session start, prompt submit, tool use, notification, stop, and a few others. The hook runs whatever command you give it. That is the entire surface area.

Two events matter for our use case:

Notification fires when Claude is waiting for the user. Permission prompts, idle prompts, anything where the agent has yielded control back to you.

Stop fires when Claude finishes responding and is done with the turn.

Wire each event to a sound command and you have audio feedback. That is the whole idea.

The Windows installer

Save as install-claude-attention-reminder.ps1 and run with powershell -ExecutionPolicy Bypass -File install-claude-attention-reminder.ps1.

1$claudeDir = "$env:USERPROFILE\.claude"
2if (-not (Test-Path $claudeDir)) { New-Item -ItemType Directory -Path $claudeDir | Out-Null }
3
4$settingsPath = "$claudeDir\settings.json"
5
6$hooksBlock = [ordered]@{
7 Notification = @(
8   [ordered]@{
9     matcher = ""
10     hooks = @(
11       [ordered]@{
12         type = "command"
13         command = "powershell -c `"(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Exclamation.wav').PlaySync()`""
14       }
15     )
16   }
17 )
18 Stop = @(
19   [ordered]@{
20     matcher = ""
21     hooks = @(
22       [ordered]@{
23         type = "command"
24         command = "powershell -c `"(New-Object Media.SoundPlayer 'C:\Windows\Media\tada.wav').PlaySync()`""
25       }
26     )
27   }
28 )
29}
30
31function ConvertTo-Hashtable {
32 param($obj)
33 if ($null -eq $obj) { return $null }
34 if ($obj -is [System.Collections.IEnumerable] -and $obj -isnot [string]) {
35   return @($obj | ForEach-Object { ConvertTo-Hashtable $_ })
36 }
37 if ($obj -is [psobject]) {
38   $ht = [ordered]@{}
39   $obj.PSObject.Properties | ForEach-Object { $ht[$_.Name] = ConvertTo-Hashtable $_.Value }
40   return $ht
41 }
42 return $obj
43}
44
45if (Test-Path $settingsPath) {
46 try {
47   $raw = Get-Content $settingsPath -Raw
48   if ([string]::IsNullOrWhiteSpace($raw)) {
49     $existing = [ordered]@{}
50   } else {
51     $existing = ConvertTo-Hashtable (ConvertFrom-Json $raw)
52   }
53 } catch {
54   Copy-Item $settingsPath "$settingsPath.bak" -Force
55   $existing = [ordered]@{}
56 }
57 $existing["hooks"] = $hooksBlock
58 $existing | ConvertTo-Json -Depth 10 | Set-Content $settingsPath -Encoding UTF8
59} else {
60 ([ordered]@{ hooks = $hooksBlock }) | ConvertTo-Json -Depth 10 | Set-Content $settingsPath -Encoding UTF8
61}
62
63Write-Host "Done. Restart Claude Code to load the hooks." -ForegroundColor Green
64(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Exclamation.wav').PlaySync()
65(New-Object Media.SoundPlayer 'C:\Windows\Media\tada.wav').PlaySync()

Two things worth calling out.

The ConvertTo-Hashtable function is there because Windows PowerShell 5.1 (the default on most Windows boxes) does not have ConvertFrom-Json -AsHashtable. That flag arrived in PowerShell 7. The manual converter means the same script runs on stock Windows installs without forcing anyone to upgrade their shell.

The script also backs up an existing settings.json to settings.json.bak if it cannot parse the file, instead of erroring out and leaving you in a broken state. Small thing, but it matters when this is running on someone else's machine.

The macOS installer

Same logic, different shell. Save as install-claude-attention-reminder.sh.

1#!/bin/bash
2
3CLAUDE_DIR="$HOME/.claude"
4SETTINGS="$CLAUDE_DIR/settings.json"
5
6mkdir -p "$CLAUDE_DIR"
7
8if [ -f "$SETTINGS" ]; then
9 cp "$SETTINGS" "$SETTINGS.bak"
10fi
11
12HOOKS_JSON='{
13 "hooks": {
14   "Notification": [
15     {
16       "matcher": "",
17       "hooks": [
18         {
19
"type": "command",
20
"command": "afplay /System/Library/Sounds/Purr.aiff"
21         }
22       ]
23     }
24   ],
25   "Stop": [
26     {
27       "matcher": "",
28       "hooks": [
29         {
30
"type": "command",
31
"command": "afplay /System/Library/Sounds/Glass.aiff"
32         }
33       ]
34     }
35   ]
36 }
37}'
38
39if [ -f "$SETTINGS" ] && command -v jq >/dev/null 2>&1; then
40 TMP=$(mktemp)
41 jq --argjson new "$HOOKS_JSON" '. + $new' "$SETTINGS" > "$TMP" && mv "$TMP" "$SETTINGS"
42else
43 echo "$HOOKS_JSON" > "$SETTINGS"
44fi
45
46echo "Done. Restart Claude Code to load the hooks."
47afplay /System/Library/Sounds/Purr.aiff
48afplay /System/Library/Sounds/Glass.aiff

Run with chmod +x install-claude-attention-reminder.sh && ./install-claude-attention-reminder.sh.

afplay ships with macOS. The sound files in /System/Library/Sounds/ ship with macOS. Zero installs. If jq is on the machine, the script merges hooks into existing settings. If not, it writes a fresh file (after backing up the old one).

What we tried and dropped: the recurring reminder

The single ding works, but it has an obvious limit. If you miss the sound (headphones off, on a call, in another room), Claude waits forever and the moment passes. Fire and forget.

So we tried building an escalating reminder. The idea: when Claude needs attention, start a background loop that plays a gentle chime, then a louder one, eventually a phone-style ring every five seconds until you respond.

It looked clean on paper. The Notification hook starts a background process that creates a lock file and loops through escalation stages. The UserPromptSubmit and Stop hooks delete the lock file. The loop checks for the lock file every second and exits when it disappears.

It did not survive contact with reality. The lock file pattern broke in several ways.

The lock file occasionally did not get deleted. Hooks run as separate processes, and if the events fired in an unexpected order, the cleanup hook would not always execute. The result was an orphaned background process playing escalating sounds with nothing to stop it except killing the process manually.

Background process detachment is fragile across shells. On Windows, Start-Process with -WindowStyle Hidden mostly works, but every now and then the background loop would inherit the terminal's standard input and block Claude Code from continuing until you killed it. On macOS, nohup ... & is more reliable, but the team is on both platforms, and inconsistent behavior across machines is its own bug.

Hooks fire on a different cadence than you'd expect. The Notification event has internal gating (it does not fire instantly for idle waits), which means in practice the escalation either started too late to be useful, or it kept firing on every permission prompt and stacked multiple reminder loops on top of each other.

The whole thing was clever and not robust. We pulled it. The single ding is what the team is using now.

The honest reasoning: if you are at your desk, the single ding catches you. If you have stepped away long enough to miss it, you are not at the keyboard anyway, and a louder sound would not help. When you come back to the desk, you glance at the terminal and see Claude is waiting. The audio is there to catch you when you are actually present, not to summon you from across the building. The Stop hook fires reliably every time a task completes, which turns out to be the more useful of the two events in practice.

If someone has an escalating reminder that actually holds up without lock files, we are genuinely curious. The idea is right. The mechanism we tried just was not.

Why we did not use the existing community plugins

There are several solutions for this floating around GitHub and dev blogs. We looked at them. Here is why we built our own anyway.

The TypeScript hook repos. The most popular one is Pascal Poredda's awesome-claude-code. It works. It is well written. But the hook scripts are TypeScript, which means every machine running them needs Node.js and npx tsx available. The install script is bash, so on Windows you also need WSL or Git Bash. You end up adding three dependencies (Node, a TypeScript runner, a Unix shell on Windows) to play a .wav file. The complexity does not match the problem.

BurntToast for Windows toast notifications. This is the polished Windows-native option. You get real Windows 10/11 toast popups with project name, custom icons, the works. The catch is that BurntToast is a PowerShell module that has to be installed via Install-Module as Administrator, and it pulls in a chain of dependencies. Fine for a personal machine. Not great for "every dev on the team should have this set up in five minutes."

Cross-platform shell scripts. A few people have published bash scripts that detect the OS and pick the right audio command. The detection logic is solid but bash on Windows means WSL or Git Bash, which the team does not standardize on. One PowerShell installer for Windows and one bash installer for Mac is cleaner than forcing everyone through the same shell.

Built-in terminal bell. You can dump a \a into your prompt and let the terminal handle it. The problem is that the bell fires on every shell event, not just Claude Code events. You cannot tell a build completing apart from Claude waiting on you.

The pattern across all of these is the same. They solve the problem but pay for it with a dependency or a tradeoff that does not make sense for a small consulting team standardizing across machines.

What we wanted instead:

  • One file per platform
  • No external dependencies (no Node, no PowerShell modules, no WSL)
  • Idempotent (safe to run twice)
  • Backs up existing config instead of clobbering it
  • Two distinct sounds so the meaning of the alert is obvious

The native sound players (afplay on Mac, Media.SoundPlayer on Windows) hit all of these. That is the entire reason this version exists.

Two things that will trip you up

Hooks load at startup. If you save settings.json while Claude Code is already running, nothing happens. You have to fully exit and relaunch the claude CLI. Inside Claude Code, run /hooks to confirm both events are registered.

The Notification event has a delay for idle waits. If Claude pops a permission prompt the moment you submit a request, the sound fires immediately. But if Claude is waiting on you because the agent went idle (no tool use, just sitting there), the event is gated by an internal timer before it fires. The Stop hook, on the other hand, fires the instant a task completes, every time. In practice the Stop sound is the one you will hear most often, and it is the more useful of the two.

What changes after you install it

The point is not the sound. The point is that you stop context-switching to check the terminal. You can pull up Slack, jump into a doc, take a call, and trust that the moment Claude needs you or finishes, you will hear it.

For a consulting team running multiple Claude Code sessions across client projects, that small change adds up. You stop running parallel sessions in your head. The terminal tells you when it needs attention, and otherwise you can ignore it.

Five minutes of setup. Hours of focus back.

The installer scripts live in our internal ai-history-kit/scripts folder. Drop one in, run it once per machine, restart Claude Code, done.

Share on social media

Akshay Sura

Akshay Sura

Akshay is a ten-time Sitecore MVP and a two-time Kontent.ai. In addition to his work as a solution architect, Akshay is also one of the founders of SUGCON North America 2015, SUGCON India 2018 & 2019, Unofficial Sitecore Training, and Sitecore Slack.

Akshay founded and continues to run the Sitecore Hackathon. As one of the founding partners of Konabos Consulting, Akshay will continue to work with clients, leading projects and mentoring their existing teams.


Subscribe to newsletter