Files
claude-code/plugins/hookify/hooks/notification.py
Claude c57e786c15 feat: Add Notification hook handler for formatting idle notifications
Format raw JSON IPC messages from workers/teammates into user-friendly
display instead of showing raw JSON to users.

Changes:
- Add notification.py hook handler to hookify plugin that formats
  idle_notification, status_update, and progress_update messages
- Update hookify hooks.json to include Notification event handler
- Add Pattern 11 documentation for formatting teammate idle notifications
- Add format-idle-notification.sh example script

Raw JSON input like:
  {"type":"idle_notification","from":"worker-1","timestamp":"..."}

Now displays as:
  ⏺ worker-1
    ⎿ Status is idle

Slack thread: https://anthropic.slack.com/archives/C07VBSHV7EV/p1765785226571409?thread_ts=1765776251.343939&cid=C07VBSHV7EV
2025-12-15 08:01:43 +00:00

144 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Notification hook executor for hookify plugin.
This script is called by Claude Code when notifications are sent.
It formats teammate idle notifications and other IPC messages for display.
"""
import os
import sys
import json
from datetime import datetime
# CRITICAL: Add plugin root to Python path for imports
PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
if PLUGIN_ROOT:
parent_dir = os.path.dirname(PLUGIN_ROOT)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
if PLUGIN_ROOT not in sys.path:
sys.path.insert(0, PLUGIN_ROOT)
def format_idle_notification(data: dict) -> str:
"""Format an idle notification for display.
Args:
data: The notification data containing type, from, timestamp, etc.
Returns:
Formatted string for display
"""
worker_name = data.get('from', 'worker')
timestamp = data.get('timestamp', '')
# Format timestamp if present
time_str = ''
if timestamp:
try:
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
time_str = dt.strftime('%H:%M:%S')
except (ValueError, AttributeError):
time_str = ''
# Build the formatted output using the suggested format
lines = [f"{worker_name}"]
if time_str:
lines.append(f" ⎿ Status is idle ({time_str})")
else:
lines.append(" ⎿ Status is idle")
return '\n'.join(lines)
def format_notification(notification_content: str) -> dict:
"""Parse and format a notification message.
Args:
notification_content: Raw notification content (may be JSON or plain text)
Returns:
Dict with formatted systemMessage
"""
# Try to parse as JSON first
try:
data = json.loads(notification_content)
# Check if this is an idle notification
if isinstance(data, dict) and data.get('type') == 'idle_notification':
formatted = format_idle_notification(data)
return {"systemMessage": formatted}
# Handle other notification types
notification_type = data.get('type', '') if isinstance(data, dict) else ''
if notification_type == 'status_update':
worker = data.get('from', 'worker')
status = data.get('status', 'unknown')
return {"systemMessage": f"{worker}\n ⎿ Status: {status}"}
if notification_type == 'progress_update':
worker = data.get('from', 'worker')
progress = data.get('progress', '')
return {"systemMessage": f"{worker}\n{progress}"}
# For unknown JSON types, still try to format nicely
if isinstance(data, dict) and 'from' in data:
worker = data.get('from', 'worker')
msg = data.get('message', data.get('status', 'update'))
return {"systemMessage": f"{worker}\n{msg}"}
except (json.JSONDecodeError, TypeError):
# Not JSON, return as-is
pass
return {}
def main():
"""Main entry point for Notification hook."""
try:
# Read input from stdin
input_data = json.load(sys.stdin)
# Get notification content
notification = input_data.get('notification', '')
# Also check for raw notification data in the input
if not notification and input_data.get('type') == 'idle_notification':
# The input itself is an idle notification
formatted = format_idle_notification(input_data)
result = {"systemMessage": formatted}
elif notification:
# Format the notification content
result = format_notification(notification)
else:
# Check if the input looks like an IPC message
if input_data.get('type') in ['idle_notification', 'status_update', 'progress_update']:
if input_data.get('type') == 'idle_notification':
formatted = format_idle_notification(input_data)
result = {"systemMessage": formatted}
else:
worker = input_data.get('from', 'worker')
status = input_data.get('status', input_data.get('type', 'update'))
result = {"systemMessage": f"{worker}\n{status}"}
else:
result = {}
# Always output JSON
print(json.dumps(result), file=sys.stdout)
except Exception as e:
error_output = {
"systemMessage": f"Notification format error: {str(e)}"
}
print(json.dumps(error_output), file=sys.stdout)
finally:
# ALWAYS exit 0
sys.exit(0)
if __name__ == '__main__':
main()