claude-code-notifier
how it works guides faq
◐English
English العربية Deutsch Español Français עברית Italiano 日本語 한국어 Nederlands Polski Português (Brasil) Русский Türkçe Tiếng Việt 简体中文 繁體中文
Get started
All guides

The Claude Code Stop hook: run something when Claude finishes

Updated 31 October 2026 · 6 min read
on this page 01When Stop fires, and when it doesn't 02The simplest Stop hook 03What the hook receives 04Put Claude's last message in the notification 05Making Claude keep going 06Notifications for failures too 07Where Claude Code Notifier fits 08FAQ

The Claude Code Stop hook runs a command every time the main Claude agent finishes responding, which makes it the right place to send a "done" notification, log the result or run checks. It doesn't run when you interrupt Claude, and a turn that ends in an API error fires StopFailure instead. The hook receives Claude's final message in last_assistant_message, and it can return decision: "block" with a reason to make Claude carry on instead of stopping.

01When Stop fires, and when it doesn't

Stop fires at the end of every turn, not only when a whole task is complete. Claude Code has no idea of "the task"; it only knows Claude has stopped responding and is waiting for you. A short question gets one Stop, and so does a two-hour refactor.

It does not fire:

  • when you press Esc or otherwise interrupt the turn;
  • when the turn ends because of an API error such as a rate limit, overload or authentication failure. Claude Code fires StopFailure for those, with an error field you can match on (rate_limit, overloaded, server_error and others);
  • when a subagent finishes. That is SubagentStop.

Stop has no matcher support, so any matcher you add is ignored and the hook runs on every turn.

02The simplest Stop hook

Add this to ~/.claude/settings.json to run a command whenever Claude finishes. This one plays a sound on macOS:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "afplay /System/Library/Sounds/Glass.aiff"
          }
        ]
      }
    ]
  }
}

For desktop notification commands on Mac, Windows and Linux, see how to get a notification when Claude Code finishes. The rest of this guide is about what you can do with the hook's input and output.

03What the hook receives

On top of the common fields (session_id, transcript_path, cwd, permission_mode, hook_event_name), a Stop hook gets:

FieldWhat it holds
last_assistant_messageThe text of Claude's final reply for this turn
stop_hook_activetrue if Claude is already continuing because a Stop hook blocked
background_tasksTasks still running, such as background shells or subagents
session_cronsScheduled wake-ups in this session, such as from /loop

Use last_assistant_message rather than reading the transcript file. The transcript is written asynchronously and may not contain the final message yet when the hook runs.

background_tasks and session_crons tell you whether the session is really done or only paused until background work wakes it again. A notification script can check that both are empty before saying "finished".

04Put Claude's last message in the notification

This script (macOS and Linux, needs jq) shows the start of Claude's reply and stays quiet if background work is still running:

#!/bin/bash
# ~/.claude/hooks/notify-done.sh
input=$(cat)

# Skip if background tasks are still in flight
busy=$(jq '(.background_tasks // []) | length' <<<"$input")
[ "$busy" -gt 0 ] && exit 0

msg=$(jq -r '(.last_assistant_message // "Claude has finished")
             | gsub("\\s+"; " ") | .[0:140]' <<<"$input")

if command -v osascript >/dev/null 2>&1; then
  osascript -e 'on run argv' \
            -e 'display notification (item 1 of argv) with title "Claude has finished"' \
            -e 'end run' "$msg"
elif command -v notify-send >/dev/null 2>&1; then
  notify-send "Claude has finished" "$msg"
fi
exit 0

Save it, run chmod +x ~/.claude/hooks/notify-done.sh, and point the hook at it:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-done.sh" }]
      }
    ]
  }
}

The script always exits 0. That matters: on Stop, exit code 2 means "don't stop", so a notification script that exits 2 by accident would keep Claude working.

05Making Claude keep going

Stop is one of the events that can block. Blocking a stop means Claude carries on with the turn, using your reason as its next instruction. You can do this two ways:

  • exit 2, and Claude receives your stderr as the reason; or
  • exit 0 and print JSON:
{
  "decision": "block",
  "reason": "The test suite is failing. Run npm test, fix the failures, then finish."
}

If the hook is giving guidance rather than reporting an error, hookSpecificOutput.additionalContext also keeps the conversation going, but the transcript labels it as Stop hook feedback instead of a hook error:

{
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "additionalContext": "Please run the test suite before finishing"
  }
}

Avoiding an endless loop

A Stop hook that always blocks would never let Claude stop. Two things protect you:

  1. stop_hook_active is true once Claude is already continuing because of a Stop hook. Check it and exit early.
  2. Claude Code ends the turn anyway after Stop hooks have blocked eight times in a row with no tool call in between. The CLAUDE_CODE_STOP_HOOK_BLOCK_CAP environment variable raises that cap.

Here is a hook that makes Claude fix failing tests once, then lets it stop:

#!/bin/bash
# ~/.claude/hooks/tests-must-pass.sh
input=$(cat)
[ "$(jq -r '.stop_hook_active' <<<"$input")" = "true" ] && exit 0

if ! npm test --silent >/dev/null 2>&1; then
  jq -nc '{decision: "block", reason: "npm test is failing. Fix the failures, then finish."}'
fi
exit 0

Hooks run in the session's working directory, so npm test runs in your project. Keep checks fast: by default a command hook can run for up to 600 seconds, and Claude waits for it.

If you just want Claude to keep working towards a goal without writing a script, Claude Code's built-in /goal command sets up a session-scoped Stop hook for you.

06Notifications for failures too

Since API errors skip Stop, a "done" alert won't tell you Claude gave up on a rate limit. Add the same script under StopFailure if you want to hear about that. Its output and exit code are ignored, so it can only notify or log. For StopFailure, last_assistant_message holds the error text, such as "API Error: Rate limit reached".

07Where Claude Code Notifier fits

A Stop hook that calls a push service is a perfectly good way to know Claude has finished while you are away. If you want to do it yourself, the ntfy guide is a good start.

Claude Code Notifier uses the same Stop hook, with one difference: it can send something back. Its free command line tool, ccn, runs on Mac, Windows and Linux and shows a desktop notification on its own. With the iPhone or Android app paired, you get a "Claude has finished" push the moment a turn ends, and you can type the next instruction on the phone. ccn returns it from the Stop hook as Claude's next instruction, so the session carries on without you at the keyboard. If you typed in the last 30 seconds, nothing waits for the phone. It is independent and not made by Anthropic. See walking away from long tasks for that workflow and the ccn page to install.

08FAQ

Does the Stop hook run when Claude finishes the whole task, or after every reply?

After every reply. Stop fires whenever the main agent finishes responding, whatever the size of the request. Check background_tasks and session_crons in the input if you need to know whether more work is still scheduled.

Why didn't my Stop hook fire?

You may have interrupted the turn, which skips Stop, or it ended with an API error, which fires StopFailure instead. Otherwise check /hooks lists your hook and that the script is executable. The troubleshooting checklist covers the rest.

Why does Claude keep working after my Stop hook runs?

Your hook is blocking the stop, either by exiting with code 2 or by printing decision: "block". Make sure a notification-only hook always exits 0, and that any blocking hook checks stop_hook_active first.

How do I get Claude's final answer in the hook?

Read last_assistant_message from the JSON on stdin, for example with jq -r '.last_assistant_message'. Don't rely on transcript_path: the file may not contain the last message yet when Stop fires.

What is the difference between Stop and SubagentStop?

Stop is for the main agent. SubagentStop fires when a subagent finishes, matches on the agent type, and supports the same blocking fields. A "Claude has finished" alert belongs on Stop.

Claude Code Notifier Get a push on your phone when Claude Code finishes or needs you, then approve, deny or answer from there. The ccn command line tool is free on Mac, Windows and Linux.
Download for iOS Download for Android Install ccn

Related guides

  • How to get a notification when Claude Code finishesUse a Stop hook for "finished" and a Notification hook for "needs input". Copy-paste setups for Mac, Windows and Linux, plus phone alerts.
  • Claude Code hooks explained: every event and what it is forWhat Claude Code hooks are, where they go in settings.json, how matchers, input and exit codes work, and what each hook event is for.
  • The Claude Code Notification hook: when it fires and how to use itWhen Claude Code's Notification hook fires, what permission_prompt and idle_prompt mean, the input it gets, and why it waits before firing.
  • Leave Claude Code running and walk away: a workflowA practical routine for long Claude Code tasks: keep the computer awake, cut needless prompts, get pinged on your phone and reply from there.
claude-code-notifier

Claude Code Notifier is an independent third-party application. It is not affiliated with, endorsed by, or sponsored by Anthropic, PBC. "Claude" and "Claude Code" are trademarks of Anthropic, PBC.

apps iOS App Android App ccn for macOS, Windows & Linux All guides
legal Privacy Policy Terms of Service Other languages © 2026 touchtap. All rights reserved. TouchTap Ltd is registered in England and Wales, company number 13942589.