Skip to content

Completion Hooks and Notifications

Get notified when Claude finishes. Play a sound, send a message, trigger a build. Turn Claude into a background worker.

You give Claude Code a big task, switch to another window, and twenty minutes later you wonder if it's done. You check. Finished fifteen minutes ago. That's dead time you're never getting back.

Completion hooks fix this. Two hook events, two different jobs.

Notification hooks fire when Claude Code needs your attention. Four triggers: permission_prompt (it wants to run a tool and needs approval), idle_prompt (waiting for your next instruction), auth_success (authentication completed), and elicitation_dialog (it's asking you a question). These are "hey, come back" signals.

Stop hooks fire when Claude Code finishes responding. Every completed turn triggers your stop hook. Kick off builds, send messages, run linters. Notification hooks say "I need you." Stop hooks say "I'm done."

Both live in your settings file, either globally in user settings or per-project in .claude/settings.json.

Setting Up Notifications

Easiest starting point: a desktop notification. Pop up an OS-level message when Claude Code finishes or needs you. The command varies by platform.

On macOS:

osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'

On Linux:

notify-send 'Claude Code' 'Claude Code needs your attention'

On Windows:

powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')"

To wire this up, add it to your settings JSON. macOS example that fires on every notification event:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

The empty matcher string means "match everything." Only want permission prompts? Set "matcher": "permission_prompt". You can also type /hooks inside a session to configure this interactively.

Prefer sound over visuals? On macOS, use afplay /System/Library/Sounds/Glass.aiff. On Linux, paplay /usr/share/sounds/freedesktop/stereo/complete.oga. Chain both if you want. Sound plus notification. Belt and suspenders.

Stop Hooks for Post-Completion Actions

Stop hooks are where the real power sits. They run after Claude Code finishes, so you can chain anything that should happen next.

Run your linter after every response:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "npm run lint"
          }
        ]
      }
    ]
  }
}

Send a Slack message via webhook when a task completes:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"Claude Code finished a task\"}' https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
          }
        ]
      }
    ]
  }
}

There's also a "prompt" type for stop hooks. Instead of running a shell command, it injects a follow-up prompt back into Claude Code. A prompt-type stop hook that says "Did you run the tests? If not, run them now" prevents Claude Code from calling it quits prematurely on complex tasks. Sneaky powerful.

The Background Worker Pattern

Here's the actual workflow that makes it all click:

  1. Give Claude Code a big task. "Refactor the auth module to use JWT, update all tests, make sure the build passes."
  2. Switch to another terminal tab. Another project. Go make coffee. Whatever.
  3. Notification hook pings you when Claude Code needs permission or finishes.
  4. Come back, review, move on.

You're not babysitting. You're delegating.

The trick is combining both hook types. Notification hooks tell you when to come back. Stop hooks automatically run your build or test suite, so by the time you return, you already know if the result is good or garbage.

Try It

Set up a sound notification right now. Create or edit .claude/settings.json in your home directory:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "powershell.exe -Command \"[console]::beep(1000,500)\""
          }
        ]
      }
    ]
  }
}

Swap the command for your platform if you're on macOS or Linux. Start a session, give Claude Code a small task, switch away. You should hear the notification when it needs you. Once that works, add a stop hook that runs your linter or build. Build up from there.

Once you've got both notification and stop hooks running together, Claude Code basically becomes a background worker. You fire off a task, go do something else, get pinged when it's done, and by the time you look at the output your build or linter has already run. No babysitting required.

Mastering Claude Code

Lesson 43 of 70