ai
3 мин
23 августа 2026 г.
Источник: Dev.to AI Feed

How to Get Notified When Claude Code Finishes a Task

shahab
shahab
RSS AI Ingest
How to Get Notified When Claude Code Finishes a Task

You start a task in Claude Code. It will take a few minutes, so you switch to Ten minutes later you check back. It finished after ninety seconds. Or worse: it The fix is about fifteen lines of shell script. But the beep is the easy part. Th...

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. Why you cannot just ask Claude to tell you 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. What a hook is 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. These are the events you are most likely to use: 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 Hooks go in settings.json, grouped by event: { "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify.sh done", "async": true, "timeout": 15 }] }] } } 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. The first version Here is the whole thing: #!/bin/bash afplay /System/Library/Sounds/Glass.aiff say "Claude is done" Attach it to Stop and it works immediately. It is also annoying within an hour. Three separate reasons. Problem 1: it does not tell you which session 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. The hook input solves this. It includes cwd, the working directory: project=$(jq -r '.cwd' "$statedir/$session.start" Read it when the turn ends: [ -f "$stamp" ] || exit 0 elapsed=$(( $(date +%s) - $(cat "$stamp") )) rm -f "$stamp" [ "$elapsed" -lt "$MIN_SECONDS" ] && exit 0 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. Gate two: are you already watching? This is what makes the tool worth keeping. Two questions, and both must be true for it to stay silent. How long since you touched the computer? ioreg -c IOHIDSystem | awk '/HIDIdleTime/ {print int($NF/1000000000); exit}' Which application is in front? osascript -e 'tell application "System Events" \ to get name of first process whose frontmost is true' The second question has a catch. To compare the focused app against your session, the script needs to know which app owns the session. That is harder than it sounds. $TERM_PROGRAM looks promising but lies — both Cursor and Windsurf report vscode. Walking the process tree is exact. The hook's parent processes lead all the way up to the application that started everything: zsh → claude → zsh → Code Helper → Code.app/Contents/MacOS/Code Collect the name of every ancestor. If the focused application matches any of them, you are looking at this session. No process is ever called zsh or launchd in the window server, so wrong matches are not a risk. Put together, the logic is: Stop fires ├─ No timestamp? → silent (/clear, /compact, resume) ├─ Ran under 60 seconds? → silent (you barely left) ├─ App focused + active? → silent (you can see it already) └─ Anything else → play the sound "Needs input" skips the timing check — a blocked session needs you no matter how quickly it got stuck — but it still respects the focus check. What I could not make work Reading window titles would let the script tell tabs apart. macOS blocks it: execution error: osascript is not allowed assistive access. (-1719) So it can identify the application, but not the tab. Two Claude sessions in two tabs of one terminal look the same to the script. Focus that terminal and both stay quiet, even though only one is visible. Granting Accessibility permission would fix it. I left it out, because asking for a broad system permission during install is a poor trade for the benefit. This is a real limit, not a detail that goes away if you ignore it. Notification tools that promise more than they deliver get uninstalled. Turning it into a plugin Copying a script into ~/.claude/hooks/ is fine for one machine. To share it, you need a plugin: claude-notify/ ├── .claude-plugin/marketplace.json └── claude-notify-plugin/ ├── .claude-plugin/plugin.json ├── hooks/ │ ├── hooks.json │ └── notify.sh └── commands/claude-notify.md Use ${CLAUDE_PLUGIN_ROOT} for file paths inside hooks.json. The plugin is installed into a cache directory, and you do not control where that is. That same fact drives the one decision worth copying: a plugin script is not editable by the user. It gets overwritten every time the plugin updates. My local version had settings at the top of the file. That is perfect for one machine and useless for sharing. Every setting had to become an environment variable: MIN_SECONDS=${CLAUDE_NOTIFY_MIN_SECONDS:-60} IDLE_SECONDS=${CLAUDE_NOTIFY_IDLE_SECONDS:-30} PRESENCE=${CLAUDE_NOTIFY_PRESENCE:-1} Users set those in their own settings.json, where an update cannot overwrite them. Two more rules for anything you plan to share: Degrade, do not fail. No paplay? Try aplay, then fall back to the terminal bell. No jq? Keep a sed fallback for reading the payload. Cannot read idle time? Skip the focus check and alert anyway. A missed alert is worse than an extra one, so every unknown should resolve toward telling the user. Always exit 0. A hook that returns an error can interfere with the session it is attached to. A notifier has no business doing that. Testing it Check the manifests: claude plugin validate ./claude-notify-plugin claude plugin validate . Test the logic by feeding the script fake input, instead of starting real sessions and waiting: echo '{"session_id":"t","cwd":"/repo"}' | bash notify.sh done One warning from experience. I wrote a test script that passed environment variables through a shell variable, and spent a while chasing a bug that did not exist. zsh does not split unquoted variables into separate words the way bash does, so env $VARS command silently collapsed into one long assignment. The script was fine; my test was wrong. When a test fails, check the test before you change the code. The takeaway Hooks are how you make things happen reliably in Claude Code, because they are not the model's decision. Timing, alerts, formatting, logging — anything that must happen every time belongs in a hook. And if you build a notifier, remember that the sound is the easy part. The value is in every case where it decides to say nothing. How this was built Built and written with Claude Code, directed by me. The tool is real, runs on my machine, and every gate described here was tested with the synthetic payloads shown above. The Accessibility limitation is one I actually hit, not a hypothetical. Claude Notify is MIT licensed and available at github.com/shahabyounas/claude-notify. /plugin marketplace add shahabyounas/claude-notify /plugin install claude-notify@claude-notify

Хотите внедрить ИИ в ваш бренд?

Спроектируем и развернем автономных агентов и современный цифровой стек под ваши задачи.

Рассчитать проект