8.3 KiB
8.3 KiB
Hook Development Patterns
Comprehensive guide for creating event-driven automation with hooks.
Hook Configuration
Basic Structure
{
"hooks": [
{
"event": "file-changed",
"pattern": "**/*.py",
"script": "hooks/python_linter.sh"
}
]
}
Available Events
file-changed
Triggered when files are modified in the workspace.
{
"event": "file-changed",
"pattern": "**/*.{js,jsx,ts,tsx}",
"script": "hooks/format_code.sh",
"description": "Format JavaScript/TypeScript files"
}
Event Data:
CHANGED_FILE: Path to modified fileFILE_EXTENSION: File extensionPROJECT_ROOT: Project root directory
git-commit-msg-needed
Triggered when creating git commits.
{
"event": "git-commit-msg-needed",
"script": "hooks/generate_commit_msg.py",
"description": "Generate conventional commit messages"
}
Event Data:
STAGED_FILES: List of staged filesDIFF_CONTENT: Git diff contentBRANCH_NAME: Current branch
task-completed
Triggered after task completion.
{
"event": "task-completed",
"pattern": "*deploy*",
"script": "hooks/notify_team.sh",
"description": "Notify team of deployment completion"
}
Event Data:
TASK_NAME: Completed task nameTASK_DURATION: Execution timeTASK_STATUS: Success/failure
Pattern Matching
Glob Patterns
{
"pattern": "**/*.py", // All Python files
"pattern": "src/**/*.test.js", // Test files in src
"pattern": "*.config.{js,json}", // Config files
"pattern": "!**/*.min.js" // Exclude minified
}
Multiple Patterns
{
"event": "file-changed",
"patterns": [
"**/*.py",
"**/*.pyi"
],
"script": "hooks/python_type_check.sh"
}
Regex Patterns
{
"event": "task-completed",
"pattern_type": "regex",
"pattern": "^deploy-.*-prod$",
"script": "hooks/production_monitor.sh"
}
Script Implementation
Shell Script Example
#!/bin/bash
# hooks/format_code.sh
set -e
# Access event data
FILE_PATH="$CHANGED_FILE"
EXTENSION="$FILE_EXTENSION"
# Format based on file type
case "$EXTENSION" in
js|jsx|ts|tsx)
npx prettier --write "$FILE_PATH"
;;
py)
black "$FILE_PATH"
;;
go)
gofmt -w "$FILE_PATH"
;;
esac
echo "Formatted: $FILE_PATH"
Python Script Example
#!/usr/bin/env python3
# hooks/generate_commit_msg.py
import os
import json
import subprocess
def generate_commit_message():
staged_files = os.environ.get('STAGED_FILES', '').split('\n')
diff_content = os.environ.get('DIFF_CONTENT', '')
# Analyze changes
file_types = set()
for file in staged_files:
if file.endswith('.py'):
file_types.add('python')
elif file.endswith(('.js', '.tsx')):
file_types.add('frontend')
# Generate message
if 'python' in file_types and 'frontend' in file_types:
prefix = 'feat(fullstack):'
elif 'python' in file_types:
prefix = 'feat(backend):'
else:
prefix = 'feat(frontend):'
return f"{prefix} Update {len(staged_files)} files"
if __name__ == "__main__":
message = generate_commit_message()
print(message)
Node.js Script Example
#!/usr/bin/env node
// hooks/validate_json.js
const fs = require('fs');
const path = require('path');
const filePath = process.env.CHANGED_FILE;
if (filePath.endsWith('.json')) {
try {
const content = fs.readFileSync(filePath, 'utf8');
JSON.parse(content);
console.log(`✓ Valid JSON: ${path.basename(filePath)}`);
} catch (error) {
console.error(`✗ Invalid JSON: ${filePath}`);
console.error(error.message);
process.exit(1);
}
}
Advanced Patterns
Conditional Execution
{
"event": "file-changed",
"pattern": "**/*.py",
"script": "hooks/conditional_lint.sh",
"conditions": {
"branch_pattern": "feature/*",
"skip_patterns": ["**/migrations/*"]
}
}
Chained Hooks
{
"hooks": [
{
"event": "file-changed",
"pattern": "**/*.py",
"script": "hooks/format.sh",
"order": 1
},
{
"event": "file-changed",
"pattern": "**/*.py",
"script": "hooks/lint.sh",
"order": 2
},
{
"event": "file-changed",
"pattern": "**/*.py",
"script": "hooks/type_check.sh",
"order": 3
}
]
}
Debouncing
{
"event": "file-changed",
"pattern": "**/*.scss",
"script": "hooks/compile_sass.sh",
"debounce": 1000,
"description": "Compile SASS after 1s of no changes"
}
Security Best Practices
Input Validation
#!/bin/bash
# Validate file paths
FILE="$CHANGED_FILE"
# Check for directory traversal
if [[ "$FILE" == *".."* ]]; then
echo "Error: Invalid file path"
exit 1
fi
# Verify file exists in project
if [[ ! "$FILE" == "$PROJECT_ROOT"* ]]; then
echo "Error: File outside project"
exit 1
fi
Safe Command Execution
#!/usr/bin/env python3
import subprocess
import shlex
import os
def run_command_safely(cmd, file_path):
# Sanitize file path
safe_path = shlex.quote(file_path)
# Use subprocess safely
result = subprocess.run(
[cmd, safe_path],
capture_output=True,
text=True,
timeout=30
)
return result.returncode == 0
Environment Variable Safety
#!/bin/bash
# Sanitize environment variables
# Remove potentially dangerous characters
SAFE_BRANCH=$(echo "$BRANCH_NAME" | tr -cd '[:alnum:]/_-')
# Validate expected format
if [[ ! "$SAFE_BRANCH" =~ ^[a-zA-Z0-9/_-]+$ ]]; then
echo "Error: Invalid branch name"
exit 1
fi
Performance Optimization
Async Processing
// hooks/async_process.js
const { Worker } = require('worker_threads');
async function processFileAsync(filePath) {
return new Promise((resolve, reject) => {
const worker = new Worker('./heavy_process.js', {
workerData: { filePath }
});
worker.on('message', resolve);
worker.on('error', reject);
});
}
Caching
# hooks/cached_analysis.py
import hashlib
import json
import os
CACHE_DIR = os.path.join(os.environ['CLAUDE_PLUGIN_ROOT'], '.cache')
def get_file_hash(filepath):
with open(filepath, 'rb') as f:
return hashlib.md5(f.read()).hexdigest()
def get_cached_result(filepath):
file_hash = get_file_hash(filepath)
cache_file = os.path.join(CACHE_DIR, f"{file_hash}.json")
if os.path.exists(cache_file):
with open(cache_file, 'r') as f:
return json.load(f)
return None
Batch Processing
#!/bin/bash
# Process multiple files efficiently
# Collect files for batch processing
CHANGED_FILES=()
while read -r file; do
CHANGED_FILES+=("$file")
done < <(echo "$STAGED_FILES")
# Process in batch
if [ ${#CHANGED_FILES[@]} -gt 10 ]; then
echo "Batch processing ${#CHANGED_FILES[@]} files..."
prettier --write "${CHANGED_FILES[@]}"
else
# Process individually for small batches
for file in "${CHANGED_FILES[@]}"; do
prettier --write "$file"
done
fi
Testing Hooks
Unit Testing
# tests/test_hook.py
import unittest
import os
from hooks import generate_commit_msg
class TestCommitMsgHook(unittest.TestCase):
def test_python_files(self):
os.environ['STAGED_FILES'] = 'app.py\ntest.py'
msg = generate_commit_msg.generate()
self.assertIn('backend', msg)
Integration Testing
#!/bin/bash
# test_hooks.sh
# Test file-changed hook
echo "Testing file-changed hook..."
touch test.py
CHANGED_FILE="test.py" FILE_EXTENSION="py" ./hooks/format_code.sh
# Verify result
if black --check test.py; then
echo "✓ Hook executed successfully"
else
echo "✗ Hook failed"
exit 1
fi
Troubleshooting
Debug Mode
{
"event": "file-changed",
"pattern": "**/*.py",
"script": "hooks/lint.sh",
"debug": true,
"log_output": true
}
Common Issues
-
Hook not firing
- Check pattern matches
- Verify event name
- Ensure script is executable
-
Script errors
- Add error handling
- Check environment variables
- Verify dependencies
-
Performance issues
- Add debouncing
- Implement caching
- Use async processing
-
Security warnings
- Validate all inputs
- Use safe command execution
- Restrict file access