Sound notifications for the Cursor agent (hooks)
A while ago I described global Cursor rules for language and punctuation. Today something from a completely different shelf, but just as "lazily useful": I want the agent to let me know with a sound when it finishes working on a task or when it asks me something. Thanks to that I no longer have to stare at the chat window and can do something else in the meantime.
The solution is built on Cursor hooks. I keep all the code (the installer and the hook scripts) in a single snippet so this post does not drown in listings:
➡️ Snippet with the complete set of files: bitbucket.org/LordKhayyin/.../5qp5oj
What it gives you
Three distinct sound signals:
| When | Sound |
|---|---|
| The agent finished a turn (a plain ending). | complete.oga |
The agent finished a turn asking a question in text (the message ends with ?). |
window-question.oga |
| An action is waiting for your approval — a command outside the sandbox or a file write outside the project. | bell.oga |
All the sounds are standard freedesktop system sounds, so there is nothing extra to download.
Why hooks, not a "skill"
That was my first thought — "let's make a skill". But a skill is an instruction the agent reads while working; it is not meant to react to the agent's lifecycle events. For "do something when the agent finished / when it runs a command" you use hooks, which exchange JSON over stdin/stdout and fire on specific events.
Requirements
- Linux with a working sound server (PipeWire or PulseAudio).
- One of the players:
paplay,pw-playorffplay(the script picks the first available one). jq(to parse the hook payloads).- The
sound-theme-freedesktopsound set in/usr/share/sounds/freedesktop/stereo/.
On macOS/Windows the solution would need adapting — a different player (afplay / powershell) and different sound files. The hook logic stays the same.
Installation
User-level hooks work globally, across all projects, and live in ~/.cursor/hooks.json (event registration) and ~/.cursor/hooks/ (the scripts).
The fastest way — download install.sh from the snippet and run it:
bash install.sh
The installer creates ~/.cursor/hooks.json and ~/.cursor/hooks/*.sh, makes them executable, and backs up an existing hooks.json. Cursor reloads the configuration automatically; if the hooks do not appear, reload the window (Command Palette → "Developer: Reload Window") or check Settings → Hooks.
How it works
The solution consists of five scripts and a hooks.json file. The question-detection mechanism is more interesting than it seems, because I ran into a few traps along the way.
Finishing work and detecting a question
There is no "the agent asked a question" event. There is, however, a stop event (end of turn). The problem: the stop payload does not contain the message text, and reading the transcript is unreliable, because the model's internal "thinking" is sometimes stored there as plain text and mixes with the visible message.
The solution turned out to be the afterAgentResponse event, whose payload has a clean text field — just the visible message, without the reasoning. That is why the record-response.sh script (on afterAgentResponse) saves the last message's text to a state file, and notify-stop.sh (on stop) reads it, checks whether it ends with ?, and picks the "question" or "done" sound. Splitting it this way robustly handles the transcript's lag and the thinking-as-text problem.
Sound on an approval request (commands)
The beforeShellExecution event has a sandbox field in its payload. When it is false, the command runs outside the sandbox and Cursor asks for approval — then approval-notify.sh plays bell. When it is true, the command goes to the sandbox without a prompt and there is silence.
Watch out for the jq trap: the // operator treats the value false the same as null, so .sandbox // "true" for sandbox:false would return "true" and invert the logic. You have to read the raw value with jq -r '.sandbox'.
Sound on an approval request (file writes)
File tools have no approval flag, but they go through preToolUse. In practice Cursor pauses for approval on writes outside the project directories. That is why file-approval-notify.sh compares the target path against the workspace directories and plays bell when a write targets outside the project. This event fires before the approval prompt, so the sound arrives at the right moment.
Limitations (deliberate trade-offs)
- The options question popup (
AskQuestion) gives no sound. That interactive tool emits no hook, andstoponly arrives after your answer. That is why I detect questions by text ending with?, not by the popup. - The approval sound can be over-eager.
bellplays for every command outside the sandbox and every write outside the project — including when the given path or command is on your allowlist and runs without a prompt. The payloads do not carry a field that would distinguish "auto-approved" from "a real approval request". - I skip file reads. Reads usually do not pause the action, so playing on them would be just noise.
Customization
- Different sounds — change the name in the
play-sound.shcall argument (e.g.bell→dialog-warning). You will find the available names in/usr/share/sounds/freedesktop/stereo/. - Disable the approval sound for commands — remove the
beforeShellExecutionsection fromhooks.json. - Disable the approval sound for files — remove the
preToolUsesection.
Summary
A few small scripts and one configuration file make the agent stop working "in silence": I hear when it finished, when it asks about something, and when it waits for my approval. The rest of my attention stays free for other things. The complete set of files is in the Bitbucket snippet.