438 lines
8.3 KiB
Markdown
438 lines
8.3 KiB
Markdown
# Hook Development Patterns
|
|
|
|
Comprehensive guide for creating event-driven automation with hooks.
|
|
|
|
## Hook Configuration
|
|
|
|
### Basic Structure
|
|
```json
|
|
{
|
|
"hooks": [
|
|
{
|
|
"event": "file-changed",
|
|
"pattern": "**/*.py",
|
|
"script": "hooks/python_linter.sh"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## Available Events
|
|
|
|
### file-changed
|
|
Triggered when files are modified in the workspace.
|
|
|
|
```json
|
|
{
|
|
"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 file
|
|
- `FILE_EXTENSION`: File extension
|
|
- `PROJECT_ROOT`: Project root directory
|
|
|
|
### git-commit-msg-needed
|
|
Triggered when creating git commits.
|
|
|
|
```json
|
|
{
|
|
"event": "git-commit-msg-needed",
|
|
"script": "hooks/generate_commit_msg.py",
|
|
"description": "Generate conventional commit messages"
|
|
}
|
|
```
|
|
|
|
**Event Data**:
|
|
- `STAGED_FILES`: List of staged files
|
|
- `DIFF_CONTENT`: Git diff content
|
|
- `BRANCH_NAME`: Current branch
|
|
|
|
### task-completed
|
|
Triggered after task completion.
|
|
|
|
```json
|
|
{
|
|
"event": "task-completed",
|
|
"pattern": "*deploy*",
|
|
"script": "hooks/notify_team.sh",
|
|
"description": "Notify team of deployment completion"
|
|
}
|
|
```
|
|
|
|
**Event Data**:
|
|
- `TASK_NAME`: Completed task name
|
|
- `TASK_DURATION`: Execution time
|
|
- `TASK_STATUS`: Success/failure
|
|
|
|
## Pattern Matching
|
|
|
|
### Glob Patterns
|
|
```json
|
|
{
|
|
"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
|
|
```json
|
|
{
|
|
"event": "file-changed",
|
|
"patterns": [
|
|
"**/*.py",
|
|
"**/*.pyi"
|
|
],
|
|
"script": "hooks/python_type_check.sh"
|
|
}
|
|
```
|
|
|
|
### Regex Patterns
|
|
```json
|
|
{
|
|
"event": "task-completed",
|
|
"pattern_type": "regex",
|
|
"pattern": "^deploy-.*-prod$",
|
|
"script": "hooks/production_monitor.sh"
|
|
}
|
|
```
|
|
|
|
## Script Implementation
|
|
|
|
### Shell Script Example
|
|
```bash
|
|
#!/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
|
|
```python
|
|
#!/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
|
|
```javascript
|
|
#!/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
|
|
```json
|
|
{
|
|
"event": "file-changed",
|
|
"pattern": "**/*.py",
|
|
"script": "hooks/conditional_lint.sh",
|
|
"conditions": {
|
|
"branch_pattern": "feature/*",
|
|
"skip_patterns": ["**/migrations/*"]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Chained Hooks
|
|
```json
|
|
{
|
|
"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
|
|
```json
|
|
{
|
|
"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
|
|
```bash
|
|
#!/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
|
|
```python
|
|
#!/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
|
|
```bash
|
|
#!/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
|
|
```javascript
|
|
// 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
|
|
```python
|
|
# 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
|
|
```bash
|
|
#!/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
|
|
```python
|
|
# 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
|
|
```bash
|
|
#!/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
|
|
```json
|
|
{
|
|
"event": "file-changed",
|
|
"pattern": "**/*.py",
|
|
"script": "hooks/lint.sh",
|
|
"debug": true,
|
|
"log_output": true
|
|
}
|
|
```
|
|
|
|
### Common Issues
|
|
|
|
1. **Hook not firing**
|
|
- Check pattern matches
|
|
- Verify event name
|
|
- Ensure script is executable
|
|
|
|
2. **Script errors**
|
|
- Add error handling
|
|
- Check environment variables
|
|
- Verify dependencies
|
|
|
|
3. **Performance issues**
|
|
- Add debouncing
|
|
- Implement caching
|
|
- Use async processing
|
|
|
|
4. **Security warnings**
|
|
- Validate all inputs
|
|
- Use safe command execution
|
|
- Restrict file access |