Unwait

How to get notified when Claude Code finishes

· 3 min read claude code hooks macos workflow

Claude Code has a Stop hook that fires the moment a turn ends. Here is the config for a macOS notification, a sound, and a version that works with several agents at once.

Claude Code ships with a hook system, and the one you want is Stop. It fires the moment Claude finishes responding. Put a command there and you get a notification instead of watching a spinner.

Here is the smallest version that works. Open ~/.claude/settings.json and add:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Turn finished\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Restart Claude Code and the next time a turn ends you get a macOS notification. That is the whole answer for the simple case. The rest of this post is what you run into after that.

Which hook event should you use

Claude Code fires several events, and picking the wrong one is the usual reason a notification feels noisy or never arrives.

Event Fires when Good for
Stop The turn is over and Claude is done "It finished, come back"
Notification Claude needs you: a permission prompt or input "It is blocked on you"
UserPromptSubmit You submit a prompt Marking the start of a wait
SubagentStop A subagent finishes Usually too chatty

Most people want Stop. If you run with permission prompts on, add Notification too, because a blocked agent is not a finished agent and you want to know the difference.

Make it a sound instead

Notifications stack up and get ignored. A short sound is often better, and macOS ships with a set of them:

{
  "type": "command",
  "command": "afplay /System/Library/Sounds/Glass.aiff"
}

/System/Library/Sounds/ has Glass, Ping, Hero, Submarine, and a few more. Pick a quiet one. If you are in a terminal that supports it, printf '\a' gives you the plain bell with no dependency at all.

The part that breaks: hooks run in your critical path

A hook is a command Claude Code runs and waits for. If your command is slow, Claude waits. If your command hangs, so does your session.

Two rules keep this from biting you:

The pattern we use looks like this:

#!/bin/sh
# Read the hook JSON off stdin, fire and forget, always succeed.
payload="$(cat 2>/dev/null)"
curl -s -m 1 -X POST --data "$payload" http://127.0.0.1:4242/wait/end >/dev/null 2>&1 &
exit 0

exit 0 at the end matters. A hook that exits non-zero can surface as an error in your session, and a notification script is never worth failing a turn over.

How do you know which agent finished

This is where the one-line notification falls apart. If you run three agents in three terminals, "Turn finished" tells you nothing useful.

The fix is in the payload. Claude Code passes a JSON object on stdin with fields including session_id, cwd, and hook_event_name. The working directory is what you want, because the last path segment is usually the project name:

#!/bin/sh
payload="$(cat)"
project=$(printf '%s' "$payload" | /usr/bin/python3 -c \
  'import json,sys,os;print(os.path.basename(json.load(sys.stdin).get("cwd","")))')
osascript -e "display notification \"$project finished\" with title \"Claude Code\"" &
exit 0

Now the notification says which project landed. You still have to find that window yourself, which is the next problem, and there is no hook for that one.

What about Codex

Codex does not use the same hook system. It has a single notify key in ~/.codex/config.toml that runs a command when a turn completes, passing a JSON string as the first argument rather than on stdin:

notify = ["/Users/you/bin/codex-notify.sh"]

There is no start event, so you can tell when Codex finished but not when it began.

A word of caution on settings.json

~/.claude/settings.json is a normal JSON file that other tools also write to. Two things worth doing before you edit it:

If you add hooks programmatically, append to the array for that event rather than replacing it. Overwriting is how people lose the Slack notifier they set up months ago.

Further reading

The hook events and payload fields are documented in the Claude Code hooks reference.

Unwait does this for you

A macOS menu bar app that watches your Claude Code and Codex sessions, shows a short card while they work, and puts a strip on screen the moment one finishes. Free for two weeks, no card and no sign up.

Try for free
← All posts