You start a task in Claude Code. It will take a few minutes, so you switch to something else while you wait. Then you get absorbed in the new task and forget about the first one.
Ten minutes later you check back. It finished after ninety seconds. Or worse: it stopped after twenty seconds to ask you a simple yes/no question, and has been sitting there waiting the whole time.
The fix is about fifteen lines of shell script. But the beep is the easy part. The hard part is teaching it when to stay quiet.
This post walks through building Claude Notify, a plugin that solves this. Everything here works on macOS and Linux.
The obvious approach is to tell Claude "let me know when you are done." This does not work, and the reason is worth understanding.
Anything you put in a prompt, a CLAUDE.md file, or a memory is a request to the model. The model has to choose to follow it. And once the turn is over, the model is not running at all — which is exactly the moment you care about.
You need something the harness runs, not something the model decides to do. In Claude Code, that is a hook.
A hook is a shell command that Claude Code runs automatically when something happens in your session. The contract is simple: An event fires. Your command runs, with information about the event as JSON on standard input. Your command exits.
| Event | Fires when | |---|---| | UserPromptSubmit | You press enter | | PreToolUse / PostToolUse | Before or after a tool runs | | Notification | Claude is blocked, waiting for you | | Stop | The turn ended | | SessionStart / SessionEnd | The session opens or closes |
async: true matters. Speech takes a second or two, and without it you would wait for the sound to finish before you could type again.
Attach it to Stop and it works immediately. It is also annoying within an hour. Three separate reasons.
If you run Claude in three repositories at once, "Claude is done" tells you that something, somewhere, finished. You still have to go and look.
Stop means "the turn ended." That includes /clear, /compact, and resuming an old session. You get a sound for housekeeping that you did not care about.
This is the important one. An alert you did not need is worse than no alert, because it teaches you to ignore the ones you do need.
The Stop event does not tell you how long the turn took. But UserPromptSubmit fires when you submit, so you can measure it yourself.
That first line was meant as a safety check for a missing file. It turned out to fix Problem 2 as well.
/clear, /compact, and resume all fire Stop without a preceding UserPromptSubmit. So no timestamp gets written, the check finds nothing, and the script exits. One line, two problems solved.
This is what makes the tool worth keeping. Two questions, and both must be true for it to stay silent.
