🔔

Using Claude Code Hooks to Signal Completion and Waiting-for-Input with Sound and Notifications, Plus Applications

This article was automatically translated from theJapanese original by AI. It may contain translation errors.

Introduction

When you ask Claude Code to do work, it takes anywhere from a little to a lot of time, so you’ll want to do something else while you wait. Having a notification go off when the work is done helps reduce the time you lose. Claude Code’s Hooks feature offers a way to play a sound when Claude Code “has completed its work” or “has stopped partway through to ask the user for confirmation,” so I’ll introduce it here.

As an application, I’ll also record a way to tell which application a notification came from when you launch and run Claude Code from multiple applications like VSCode and terminals.

What are Claude Code Hooks

Quoting from the official docs, they’re defined as follows.

Hooks are user-defined shell commands, HTTP endpoints, or LLM prompts that run automatically at specific points in Claude Code's lifecycle.

Reference: https://code.claude.com/docs/en/hooks

Roughly rephrased, Hooks are a mechanism that lets you run arbitrary commands on specific events. The two events we’ll use here are Stop (when a response completes) and Notification (when waiting for user input / permission).

Basic setup to play sound and notifications

Here’s the hooks section of the ~/.claude/settings.json I actually use.

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "afplay /System/Library/Sounds/Glass.aiff; osascript -e 'display notification \"Work completed\" with title \"claude code\"' 2>/dev/null || true",
            "async": true
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "afplay /System/Library/Sounds/Funk.aiff; osascript -e 'display notification \"Confirmation needed\" with title \"claude code\"' 2>/dev/null || true",
            "async": true
          }
        ]
      }
    ]
  }
}

For the sounds, I reuse the ones bundled with macOS by default. (/System/Library/Sounds/*.aiff)

By separating the sounds by type, you can tell from the sound alone whether it finished or wants confirmation.

  • Stop: the sound of afplay /System/Library/Sounds/Glass.aiff + “Work completed” shown in the notification center via osascript
  • Notification: the sound of afplay /System/Library/Sounds/Funk.aiff + “Confirmation needed” shown in the notification center via osascript

I use osascript’s display notification so it also appears in the Mac notification center.

Application: with multiple Claudes running in parallel, the “which one finished?” problem

When you run Claude Code across multiple applications, it becomes easy to lose track of which application a notification came from.

So as an application, I want to make it possible to tell which application a notification came from, so that clicking the notification jumps you to the originating application.

I use the Claude Code VSCode extension, Claude Code running in a terminal within VSCode, the terminal app Warp, and the standard macOS terminal, so this time I’ll introduce a sample for that environment.

Show which project it is in the notification

To tell which Claude Code it is, use the JSON passed to the hook’s command via standard input. This includes the working directory cwd and session ID session_id at the time it fires, which you can extract with jq.

Putting the tail of the working directory (basename) into the notification title as the project name makes it clear at a glance which Claude Code went off.

INPUT="$(cat)"
CWD="$(printf '%s' "$INPUT" | jq -r '.cwd // empty')"
PROJECT="$(basename "${CWD:-Claude}")"   # e.g., blog
TITLE="Claude: $PROJECT"

Notification test (run in the downloads folder)

(Run in the standard macOS terminal, in the Downloads folder)

Writing the command directly in settings.json gets hard to read, so it’s better to split it out into an external script like ~/.claude/hooks/notify.sh and call that.

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "$HOME/.claude/hooks/notify.sh stop", "async": true }
        ]
      }
    ]
  }
}

Click to jump to the relevant window

I want to click the notification and jump to the relevant window, but there’s one wall here. The standard osascript -e 'display notification ...' can show a notification, but it can’t specify what happens when clicked.

So I use terminal-notifier. You can pass a command to run when clicked to the -execute option.

brew install terminal-notifier

Whether it’s the VSCode extension or the integrated terminal, the jump destination is “the VSCode window that has that workspace open.” Running code "$CWD" brings the existing window that has that working directory open to the front (if none is open, it opens a new one).

# Note: -execute runs in a shell with a minimal PATH, so specify code with an absolute path
CODE_BIN="$(command -v code)"

terminal-notifier \
  -title "$TITLE" \
  -message "Work completed" \
  -group "claude-$PROJECT" \
  -execute "$CODE_BIN '$CWD'"

Passing the same value to -group makes an old notification for the same project get replaced by the new one, so the notification center doesn’t get cluttered.

Warp is different from this approach; instead of supporting AppleScript, it has its own native notification mechanism. If you install the claude-code-warp plugin, a notification tied to a Warp tab appears, and clicking it jumps to that tab. If you use Warp, letting this handle it is the simplest.

Summary

  • Just assigning sound + notifications to Stop / Notification dramatically reduces the stress of waiting.
  • As an application, I achieved distinguishing by cwd + click-to-jump with terminal-notifier.

References

Recent Articles

Network(beta)

Drag to move / Ctrl+wheel to zoom