spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

feat: Add Notification hook handler for formatting idle notificationsBrowse files
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
Claude authored on Dec 15, 2025 parent eb87245 · commit c57e786c15a4
4 changed files+270 -0compared with eb87245
Mplugins/hookify/hooks/hooks.json
Aplugins/hookify/hooks/notification.py
Aplugins/plugin-dev/skills/hook-development/examples/format-idle-notification.sh
Mplugins/plugin-dev/skills/hook-development/references/patterns.md
@@ -44,6 +44,17 @@
4444 }
4545 ]
4646 }
47+ ],
48+ "Notification": [
49+ {
50+ "hooks": [
51+ {
52+ "type": "command",
53+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification.py",
54+ "timeout": 10
55+ }
56+ ]
57+ }
4758 ]
4859 }
4960 }
@@ -1,0 +1,143 @@
1+#!/usr/bin/env python3
2+"""Notification hook executor for hookify plugin.
3+
4+This script is called by Claude Code when notifications are sent.
5+It formats teammate idle notifications and other IPC messages for display.
6+"""
7+
8+import os
9+import sys
10+import json
11+from datetime import datetime
12+
13+# CRITICAL: Add plugin root to Python path for imports
14+PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
15+if PLUGIN_ROOT:
16+ parent_dir = os.path.dirname(PLUGIN_ROOT)
17+ if parent_dir not in sys.path:
18+ sys.path.insert(0, parent_dir)
19+ if PLUGIN_ROOT not in sys.path:
20+ sys.path.insert(0, PLUGIN_ROOT)
21+
22+
23+def format_idle_notification(data: dict) -> str:
24+ """Format an idle notification for display.
25+
26+ Args:
27+ data: The notification data containing type, from, timestamp, etc.
28+
29+ Returns:
30+ Formatted string for display
31+ """
32+ worker_name = data.get('from', 'worker')
33+ timestamp = data.get('timestamp', '')
34+
35+ # Format timestamp if present
36+ time_str = ''
37+ if timestamp:
38+ try:
39+ dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
40+ time_str = dt.strftime('%H:%M:%S')
41+ except (ValueError, AttributeError):
42+ time_str = ''
43+
44+ # Build the formatted output using the suggested format
45+ lines = [f"⏺ {worker_name}"]
46+ if time_str:
47+ lines.append(f" ⎿ Status is idle ({time_str})")
48+ else:
49+ lines.append(" ⎿ Status is idle")
50+
51+ return '\n'.join(lines)
52+
53+
54+def format_notification(notification_content: str) -> dict:
55+ """Parse and format a notification message.
56+
57+ Args:
58+ notification_content: Raw notification content (may be JSON or plain text)
59+
60+ Returns:
61+ Dict with formatted systemMessage
62+ """
63+ # Try to parse as JSON first
64+ try:
65+ data = json.loads(notification_content)
66+
67+ # Check if this is an idle notification
68+ if isinstance(data, dict) and data.get('type') == 'idle_notification':
69+ formatted = format_idle_notification(data)
70+ return {"systemMessage": formatted}
71+
72+ # Handle other notification types
73+ notification_type = data.get('type', '') if isinstance(data, dict) else ''
74+
75+ if notification_type == 'status_update':
76+ worker = data.get('from', 'worker')
77+ status = data.get('status', 'unknown')
78+ return {"systemMessage": f"⏺ {worker}\n ⎿ Status: {status}"}
79+
80+ if notification_type == 'progress_update':
81+ worker = data.get('from', 'worker')
82+ progress = data.get('progress', '')
83+ return {"systemMessage": f"⏺ {worker}\n ⎿ {progress}"}
84+
85+ # For unknown JSON types, still try to format nicely
86+ if isinstance(data, dict) and 'from' in data:
87+ worker = data.get('from', 'worker')
88+ msg = data.get('message', data.get('status', 'update'))
89+ return {"systemMessage": f"⏺ {worker}\n ⎿ {msg}"}
90+
91+ except (json.JSONDecodeError, TypeError):
92+ # Not JSON, return as-is
93+ pass
94+
95+ return {}
96+
97+
98+def main():
99+ """Main entry point for Notification hook."""
100+ try:
101+ # Read input from stdin
102+ input_data = json.load(sys.stdin)
103+
104+ # Get notification content
105+ notification = input_data.get('notification', '')
106+
107+ # Also check for raw notification data in the input
108+ if not notification and input_data.get('type') == 'idle_notification':
109+ # The input itself is an idle notification
110+ formatted = format_idle_notification(input_data)
111+ result = {"systemMessage": formatted}
112+ elif notification:
113+ # Format the notification content
114+ result = format_notification(notification)
115+ else:
116+ # Check if the input looks like an IPC message
117+ if input_data.get('type') in ['idle_notification', 'status_update', 'progress_update']:
118+ if input_data.get('type') == 'idle_notification':
119+ formatted = format_idle_notification(input_data)
120+ result = {"systemMessage": formatted}
121+ else:
122+ worker = input_data.get('from', 'worker')
123+ status = input_data.get('status', input_data.get('type', 'update'))
124+ result = {"systemMessage": f"⏺ {worker}\n ⎿ {status}"}
125+ else:
126+ result = {}
127+
128+ # Always output JSON
129+ print(json.dumps(result), file=sys.stdout)
130+
131+ except Exception as e:
132+ error_output = {
133+ "systemMessage": f"Notification format error: {str(e)}"
134+ }
135+ print(json.dumps(error_output), file=sys.stdout)
136+
137+ finally:
138+ # ALWAYS exit 0
139+ sys.exit(0)
140+
141+
142+if __name__ == '__main__':
143+ main()
@@ -1,0 +1,50 @@
1+#!/bin/bash
2+# Example: Format teammate idle notification
3+#
4+# This script demonstrates how to format raw JSON idle notifications
5+# into user-friendly display format.
6+#
7+# Usage: echo '{"type":"idle_notification","from":"worker-1","timestamp":"..."}' | ./format-idle-notification.sh
8+
9+set -euo pipefail
10+
11+# Read JSON from stdin
12+input=$(cat)
13+
14+# Parse notification type
15+notification_type=$(echo "$input" | jq -r '.type // empty' 2>/dev/null || echo "")
16+
17+if [[ "$notification_type" == "idle_notification" ]]; then
18+ # Extract fields
19+ worker_name=$(echo "$input" | jq -r '.from // "worker"')
20+ timestamp=$(echo "$input" | jq -r '.timestamp // empty')
21+
22+ # Format timestamp if present
23+ time_str=""
24+ if [[ -n "$timestamp" ]]; then
25+ # Try to format the timestamp
26+ time_str=$(date -d "$timestamp" '+%H:%M:%S' 2>/dev/null || echo "")
27+ fi
28+
29+ # Output formatted notification using recommended format:
30+ # ⏺ worker-1
31+ # ⎿ Status is idle
32+ echo "⏺ $worker_name"
33+ if [[ -n "$time_str" ]]; then
34+ echo " ⎿ Status is idle ($time_str)"
35+ else
36+ echo " ⎿ Status is idle"
37+ fi
38+
39+ # Output JSON for hook system
40+ if [[ -n "$time_str" ]]; then
41+ jq -n --arg msg "⏺ $worker_name\n ⎿ Status is idle ($time_str)" \
42+ '{"systemMessage": $msg}'
43+ else
44+ jq -n --arg msg "⏺ $worker_name\n ⎿ Status is idle" \
45+ '{"systemMessage": $msg}'
46+ fi
47+else
48+ # Not an idle notification, pass through
49+ echo "$input"
50+fi
@@ -344,3 +344,69 @@
344344 - Per-project settings
345345 - Team-specific rules
346346 - Dynamic validation criteria
347+
348+## Pattern 11: Format Teammate Idle Notifications
349+
350+Format raw JSON IPC messages from workers/teammates into user-friendly display:
351+
352+```json
353+{
354+ "Notification": [
355+ {
356+ "matcher": "*",
357+ "hooks": [
358+ {
359+ "type": "command",
360+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification.py",
361+ "timeout": 10
362+ }
363+ ]
364+ }
365+ ]
366+}
367+```
368+
369+**Example script (format-idle-notification.py):**
370+```python
371+#!/usr/bin/env python3
372+import sys
373+import json
374+
375+def format_idle_notification(data):
376+ """Format idle notification for display."""
377+ worker_name = data.get('from', 'worker')
378+ # Output format:
379+ # ⏺ worker-1
380+ # ⎿ Status is idle
381+ return f"⏺ {worker_name}\n ⎿ Status is idle"
382+
383+def main():
384+ input_data = json.load(sys.stdin)
385+
386+ # Check for idle notification
387+ if input_data.get('type') == 'idle_notification':
388+ formatted = format_idle_notification(input_data)
389+ print(json.dumps({"systemMessage": formatted}))
390+ else:
391+ print(json.dumps({}))
392+
393+if __name__ == '__main__':
394+ main()
395+```
396+
397+**Input (raw JSON IPC message):**
398+```json
399+{"type": "idle_notification", "from": "worker-1", "timestamp": "2025-12-15T05:22:40.320Z"}
400+```
401+
402+**Output (formatted for display):**
403+```
404+⏺ worker-1
405+ ⎿ Status is idle
406+```
407+
408+**Use for:**
409+- Formatting teammate/worker status messages
410+- Converting internal IPC messages to user-friendly display
411+- Multi-agent swarm coordination UI
412+- Any notification that shouldn't show raw JSON to users