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
8.4 KiB
Common Hook Patterns
This reference provides common, proven patterns for implementing Claude Code hooks. Use these patterns as starting points for typical hook use cases.
Pattern 1: Security Validation
Block dangerous file writes using prompt-based hooks:
{
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "prompt",
"prompt": "File path: $TOOL_INPUT.file_path. Verify: 1) Not in /etc or system directories 2) Not .env or credentials 3) Path doesn't contain '..' traversal. Return 'approve' or 'deny'."
}
]
}
]
}
Use for: Preventing writes to sensitive files or system directories.
Pattern 2: Test Enforcement
Ensure tests run before stopping:
{
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "prompt",
"prompt": "Review transcript. If code was modified (Write/Edit tools used), verify tests were executed. If no tests were run, block with reason 'Tests must be run after code changes'."
}
]
}
]
}
Use for: Enforcing quality standards and preventing incomplete work.
Pattern 3: Context Loading
Load project-specific context at session start:
{
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh"
}
]
}
]
}
Example script (load-context.sh):
#!/bin/bash
cd "$CLAUDE_PROJECT_DIR" || exit 1
# Detect project type
if [ -f "package.json" ]; then
echo "📦 Node.js project detected"
echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
elif [ -f "Cargo.toml" ]; then
echo "🦀 Rust project detected"
echo "export PROJECT_TYPE=rust" >> "$CLAUDE_ENV_FILE"
fi
Use for: Automatically detecting and configuring project-specific settings.
Pattern 4: Notification Logging
Log all notifications for audit or analysis:
{
"Notification": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/log-notification.sh"
}
]
}
]
}
Use for: Tracking user notifications or integration with external logging systems.
Pattern 5: MCP Tool Monitoring
Monitor and validate MCP tool usage:
{
"PreToolUse": [
{
"matcher": "mcp__.*__delete.*",
"hooks": [
{
"type": "prompt",
"prompt": "Deletion operation detected. Verify: Is this deletion intentional? Can it be undone? Are there backups? Return 'approve' only if safe."
}
]
}
]
}
Use for: Protecting against destructive MCP operations.
Pattern 6: Build Verification
Ensure project builds after code changes:
{
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "prompt",
"prompt": "Check if code was modified. If Write/Edit tools were used, verify the project was built (npm run build, cargo build, etc). If not built, block and request build."
}
]
}
]
}
Use for: Catching build errors before committing or stopping work.
Pattern 7: Permission Confirmation
Ask user before dangerous operations:
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "prompt",
"prompt": "Command: $TOOL_INPUT.command. If command contains 'rm', 'delete', 'drop', or other destructive operations, return 'ask' to confirm with user. Otherwise 'approve'."
}
]
}
]
}
Use for: User confirmation on potentially destructive commands.
Pattern 8: Code Quality Checks
Run linters or formatters on file edits:
{
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/check-quality.sh"
}
]
}
]
}
Example script (check-quality.sh):
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
# Run linter if applicable
if [[ "$file_path" == *.js ]] || [[ "$file_path" == *.ts ]]; then
npx eslint "$file_path" 2>&1 || true
fi
Use for: Automatic code quality enforcement.
Pattern Combinations
Combine multiple patterns for comprehensive protection:
{
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "prompt",
"prompt": "Validate file write safety"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "prompt",
"prompt": "Validate bash command safety"
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "prompt",
"prompt": "Verify tests run and build succeeded"
}
]
}
],
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh"
}
]
}
]
}
This provides multi-layered protection and automation.
Pattern 9: Temporarily Active Hooks
Create hooks that only run when explicitly enabled via flag files:
#!/bin/bash
# Hook only active when flag file exists
FLAG_FILE="$CLAUDE_PROJECT_DIR/.enable-security-scan"
if [ ! -f "$FLAG_FILE" ]; then
# Quick exit when disabled
exit 0
fi
# Flag present, run validation
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
# Run security scan
security-scanner "$file_path"
Activation:
# Enable the hook
touch .enable-security-scan
# Disable the hook
rm .enable-security-scan
Use for:
- Temporary debugging hooks
- Feature flags for development
- Project-specific validation that's opt-in
- Performance-intensive checks only when needed
Note: Must restart Claude Code after creating/removing flag files for hooks to recognize changes.
Pattern 10: Configuration-Driven Hooks
Use JSON configuration to control hook behavior:
#!/bin/bash
CONFIG_FILE="$CLAUDE_PROJECT_DIR/.claude/my-plugin.local.json"
# Read configuration
if [ -f "$CONFIG_FILE" ]; then
strict_mode=$(jq -r '.strictMode // false' "$CONFIG_FILE")
max_file_size=$(jq -r '.maxFileSize // 1000000' "$CONFIG_FILE")
else
# Defaults
strict_mode=false
max_file_size=1000000
fi
# Skip if not in strict mode
if [ "$strict_mode" != "true" ]; then
exit 0
fi
# Apply configured limits
input=$(cat)
file_size=$(echo "$input" | jq -r '.tool_input.content | length')
if [ "$file_size" -gt "$max_file_size" ]; then
echo '{"decision": "deny", "reason": "File exceeds configured size limit"}' >&2
exit 2
fi
Configuration file (.claude/my-plugin.local.json):
{
"strictMode": true,
"maxFileSize": 500000,
"allowedPaths": ["/tmp", "/home/user/projects"]
}
Use for:
- User-configurable hook behavior
- Per-project settings
- Team-specific rules
- Dynamic validation criteria
Pattern 11: Format Teammate Idle Notifications
Format raw JSON IPC messages from workers/teammates into user-friendly display:
{
"Notification": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification.py",
"timeout": 10
}
]
}
]
}
Example script (format-idle-notification.py):
#!/usr/bin/env python3
import sys
import json
def format_idle_notification(data):
"""Format idle notification for display."""
worker_name = data.get('from', 'worker')
# Output format:
# ⏺ worker-1
# ⎿ Status is idle
return f"⏺ {worker_name}\n ⎿ Status is idle"
def main():
input_data = json.load(sys.stdin)
# Check for idle notification
if input_data.get('type') == 'idle_notification':
formatted = format_idle_notification(input_data)
print(json.dumps({"systemMessage": formatted}))
else:
print(json.dumps({}))
if __name__ == '__main__':
main()
Input (raw JSON IPC message):
{"type": "idle_notification", "from": "worker-1", "timestamp": "2025-12-15T05:22:40.320Z"}
Output (formatted for display):
⏺ worker-1
⎿ Status is idle
Use for:
- Formatting teammate/worker status messages
- Converting internal IPC messages to user-friendly display
- Multi-agent swarm coordination UI
- Any notification that shouldn't show raw JSON to users