{"slug": "get-a-desktop-notification-when-claude-code-finishes-with-a-stop-hook", "title": "Get a desktop notification when Claude Code finishes, with a Stop hook", "summary": "A developer has documented how to configure Claude Code's Stop hook to fire a desktop notification when the AI coding agent finishes responding, distinguishing it from the Notification hook that signals Claude is waiting for input. The writeup provides platform-specific commands for macOS, Linux, and Windows, and warns that a Stop hook exiting with code 2 blocks the stop and continues the conversation, so notification commands should exit 0.", "body_md": "*Originally published at [https://aicoding-guide.com](https://aicoding-guide.com/en/posts/claude-code-hooks-stop-notify/).*\n\nWatching a terminal to see whether a long task has finished is wasted time. A notification at the moment it completes frees you to do something else in the meantime.\n\nThe event that fires when Claude finishes responding is the **`Stop` hook**. The `Notification` hook in the documentation's getting-started walkthrough is a different thing: it tells you Claude is waiting for input. This article keeps the two apart and gives the setup for all three platforms.\n\n**Key point**\n\nWhat you will learn\n\n- How\n`Stop` differs from `Notification`, and which one you want- The notification command and config for macOS, Linux and Windows\n- The exit-code trap that can keep Claude from stopping\n\nThe names are similar; the timing is not.\n\n| Event | Fires when | Matcher | \n|---|---|---|\n| `Stop` | Claude finishes responding | Not supported | \n| `SubagentStop` | A subagent finishes | Matches on agent type | \n| `Notification` | Claude Code sends a notification (permission prompt, idle, and so on) | Matches on notification type | \n\n\"Tell me when the work is done\" is `Stop`. \"Tell me when it's stuck on a permission prompt\" is `Notification`. Configuring both is fine.\n\nThe notification types you can match on include `permission_prompt`, `idle_prompt`, `auth_success`, `agent_needs_input` and `agent_completed`. The full list of matcher values per event is in [Every value you can put in a Claude Code hook matcher](https://aicoding-guide.com/en/posts/claude-code-hooks-matcher/).\n\n**A matcher on Stop is ignored**\n\n`Stop` does not support a matcher. Adding one is not an error — it is silently ignored, and the hook fires every time. To narrow it, branch inside the hook script instead.\n\nThese go in `~/.claude/settings.json`. Create the file if it does not exist.\n\n```\n{\n  \"hooks\": {\n    \"Stop\": [\n      {\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"osascript -e 'display notification \\\"Claude Code finished\\\" with title \\\"Claude Code\\\"'\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\nIf nothing appears, Script Editor — which `osascript` routes notifications through — probably lacks notification permission. The documentation notes that in that case the command fails silently and macOS never prompts you to grant it. Run this once in Terminal so Script Editor shows up in your notification settings, then enable **Allow Notifications** for **Script Editor** under **System Settings > Notifications**.\n\n```\nosascript -e 'display notification \"test\"'\n{\n  \"hooks\": {\n    \"Stop\": [\n      {\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"notify-send 'Claude Code' 'Claude Code finished'\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\n`notify-send` needs a desktop notification daemon, which headless servers, SSH sessions and most containers do not have. Test the command directly first. If it is not found, install `libnotify-bin` on Debian and Ubuntu, or your distribution's equivalent.\n\n```\nnotify-send 'Claude Code' 'test'\n{\n  \"hooks\": {\n    \"Stop\": [\n      {\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"powershell.exe -Command \\\"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code finished', 'Claude Code')\\\"\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\nRun `/hooks` afterwards to see the configured hooks per event and which file each came from. Note that the `/hooks` menu is read-only: to add, modify or remove hooks, edit the settings JSON directly or ask Claude to do it.\n\nThis is the part specific to `Stop`. **A `Stop` hook that exits with code 2 blocks the stop, and the conversation continues.** If your notification command fails and returns 2, Claude keeps going when you did not intend it to.\n\nWhen all you want is a notification, make sure the command exits 0.\n\n```\n{\n  \"type\": \"command\",\n  \"command\": \"notify-send 'Claude Code' 'Claude Code finished' || true\"\n}\n```\n\n`|| true` turns a failed notification into a 0 exit.\n\nThere is also a cap on blocking. The documentation states that Claude Code overrides a `Stop` hook after it blocks **eight times in a row without progress**. If you write a hook that deliberately blocks, read `stop_hook_active` from the JSON on stdin and exit early once you have already triggered a continuation.\n\n``` bash\n#!/bin/bash\nINPUT=$(cat)\nif [ \"$(echo \"$INPUT\" | jq -r '.stop_hook_active')\" = \"true\" ]; then\n  exit 0  # Allow Claude to stop\nfi\n# ... rest of your hook logic\n```\n\nA `Stop` hook's stdin carries `last_assistant_message` and `stop_reason`, so the notification can say what actually finished.\n\n``` bash\n#!/usr/bin/env bash\n# ~/.claude/hooks/notify-done.sh\nINPUT=$(cat)\nMSG=$(echo \"$INPUT\" | jq -r '.last_assistant_message // empty' | head -c 120)\nnotify-send 'Claude Code' \"${MSG:-Claude Code finished}\" || true\nexit 0\n{\n  \"hooks\": {\n    \"Stop\": [\n      {\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"\\\"$CLAUDE_PROJECT_DIR\\\"/.claude/hooks/notify-done.sh\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\nFor the full shape of that JSON, see [The JSON your Claude Code hooks receive on stdin](https://aicoding-guide.com/en/posts/claude-code-hooks-stdin-json/). For building hooks generally, see [Run lint and format automatically after every edit](https://aicoding-guide.com/en/posts/claude-code-hooks-lint-format/).\n\n**Glossary**\n\n**stop_reason**: a string describing why Claude stopped. A normal end of turn carries `end_turn`.\n\n`Stop` for \"work finished\" and `Notification` for \"waiting on you\"`osascript`, Linux `notify-send`, Windows a PowerShell MessageBox`|| true` when you only want a notification`stop_hook_active` to stay under the eight-block cap`last_assistant_message` lets the notification say what finished", "url": "https://wpnews.pro/news/get-a-desktop-notification-when-claude-code-finishes-with-a-stop-hook", "canonical_source": "https://dev.to/aicoding-guide/get-a-desktop-notification-when-claude-code-finishes-with-a-stop-hook-3832", "published_at": "2026-09-20 19:07:56+00:00", "updated_at": "2026-09-20 19:55:08.387867+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["Claude Code", "Anthropic", "macOS", "Linux", "Windows", "osascript", "notify-send", "PowerShell"], "alternates": {"html": "https://wpnews.pro/news/get-a-desktop-notification-when-claude-code-finishes-with-a-stop-hook", "markdown": "https://wpnews.pro/news/get-a-desktop-notification-when-claude-code-finishes-with-a-stop-hook.md", "text": "https://wpnews.pro/news/get-a-desktop-notification-when-claude-code-finishes-with-a-stop-hook.txt", "jsonld": "https://wpnews.pro/news/get-a-desktop-notification-when-claude-code-finishes-with-a-stop-hook.jsonld"}}