Sitemap

Claude Code Hooks: Automating macOS Notifications for Task Completion

4 min readJul 22, 2025

Claude Code Hooks: Automating macOS Notifications for Task Completion

What are Claude Code Hooks?

Claude Code Hooks are a powerful feature that allows you to execute specific actions based on Claude Code’s behavior. You can automatically run scripts or commands triggered by events such as when the agent completes a response or before and after tool execution.

Setting Up macOS Notifications with Claude Code Hooks

Today, I’ll show you how to display macOS notifications when Claude Code tasks are completed.

The image below shows the actual message on the Claude Code interface.

Press enter or click to view image in full size

This implementation builds upon this original article while providing more detailed notification content improvements.

Key Implementation Points

  • Uses the STOP event as a trigger
  • notify-end.sh script retrieves the latest assistant message from transcript_path
  • macOS notifications use osascript with sound alerts

Understanding the STOP Event

According to the official documentation:

STOP
Runs when the main Claude Code agent has finished responding. Does not run if the stoppage occurred due to a user interrupt.

This event executes when the Claude Code agent completes its response (but not when interrupted by the user).

Input Data Format

The Input received by the STOP event follows this format:

{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"hook_event_name": "Stop",
"stop_hook_active": true
}

💡 Debug Hook Configuration
If you want to examine the actual input data when called, you can set up a hook like this to output to a file:

{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c -r '.' >> ~/.claude/stop-log.txt"
}
]
}
]
}
}

Understanding transcript_path Content

The transcript_path contains session interactions saved in JSONL format.

Example:

{
"parentUuid":"d9d1234a-f73f-4460-b1eb-14a8e844df3d",
"isSidechain":false,
"userType":"external",
"cwd":"/Users/m.naka/repos/nakamasato/github-actions",
"sessionId":"b0004de5-676b-4ecb-a49e-23d02a39bfb8",
"version":"1.0.44",
"type":"user",
"message":{
"role":"user",
"content":[
{
"tool_use_id":"toolu_01A7e58yKYcYTYeVYJ8NgAJk",
"type":"tool_result",
"content":"Based on the GitHub releases page, the latest version of ruff-pre-commit is v0.12.2, released on 03 Jul."
}
]
},
"uuid":"f67a28e2-2bce-45f0-8545-3450a9663707",
"timestamp":"2025-07-11T11:06:58.102Z",
"toolUseResult":{
"bytes":293809,
"code":200,
"codeText":"OK",
"result":"Based on the GitHub releases page, the latest version of ruff-pre-commit is v0.12.2, released on 03 Jul.",
"durationMs":3079,
"url":"https://github.com/astral-sh/ruff-pre-commit/releases"
}
}

By checking the message.role field, you can see values of either user or assistant. To get the last message when the agent completes, I decided to extract the latest entry with role set to assistant and display it in the notification.

Implementing the Notification Script

Since multiple processing steps are required, I implemented this as a shell script.

#!/bin/bash
# ~/.claude/scripts/notify-end.sh

# Read hook Input data from standard input
INPUT=$(cat)
# Get current session directory name (hooks run in the same directory as the session)
SESSION_DIR=$(basename "$(pwd)")
# Extract transcript_path
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path')
# If transcript_path exists, get the latest assistant message
if [ -f "$TRANSCRIPT_PATH" ]; then
# Extract assistant messages from the last 10 lines and get the latest one
# Remove newlines and limit to 60 characters
MSG=$(tail -10 "$TRANSCRIPT_PATH" | \
jq -r 'select(.message.role == "assistant") | .message.content[0].text' | \
tail -1 | \
tr '\n' ' ' | \
cut -c1-60)

# Fallback if no message is retrieved
MSG=${MSG:-"Task completed"}
else
MSG="Task completed"
fi
# Display macOS notification with sound using osascript
osascript -e "display notification \"$MSG\" with title \"ClaudeCode ($SESSION_DIR) Task Done\" sound name \"Glass\""

How the Script Works

  1. Session Information Retrieval: Gets the directory where the hook is executed using pwd and uses it as the session name
  2. Message Extraction: Extracts messages with role set to assistant from the last 10 lines of transcript_path and gets the latest one
  3. Notification Display: Uses osascript to display notifications with sound effect ("Glass")

This setup allows you to see notifications with sound in the top-right notification area, showing which session completed what task when the agent finishes.

Hooks Configuration File

To make the above script work, add the following configuration to either of the following files:

  1. ~/.claude/settings.json (user scope)
  2. <project>/.claude/settings.json (project scope)
  3. <project>/.claude/settings.local.json (project local scope)
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/scripts/notify-end.sh"
}
]
}
]
}
}
Press enter or click to view image in full size
configure from /hooks command in a claude code session

Conclusion

Using Claude Code Hooks enables you to work efficiently without missing agent task completions. This feature is particularly valuable when you have long-running tasks executing in the background.

However, since getting notifications for every small task can become annoying, it might be better to only notify when agent tasks take a certain amount of time to complete.

References

--

--

Masato Naka
Masato Naka

Written by Masato Naka

An SRE, mainly working on Kubernetes. CKA (Feb 2021). His Interests include Cloud-Native application development, and machine learning.

No responses yet