Initial commit
This commit is contained in:
610
skills/building-hooks/resources/hook-patterns.md
Normal file
610
skills/building-hooks/resources/hook-patterns.md
Normal file
@@ -0,0 +1,610 @@
|
||||
# Hook Patterns Library
|
||||
|
||||
Reusable patterns for common hook use cases.
|
||||
|
||||
## Pattern: File Path Validation
|
||||
|
||||
Safely validate and sanitize file paths in hooks.
|
||||
|
||||
```bash
|
||||
validate_file_path() {
|
||||
local path="$1"
|
||||
|
||||
# Remove null/empty
|
||||
if [ -z "$path" ] || [ "$path" == "null" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Must be absolute path
|
||||
if [[ ! "$path" =~ ^/ ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Must exist
|
||||
if [ ! -f "$path" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check file extension whitelist
|
||||
if [[ ! "$path" =~ \.(ts|tsx|js|jsx|py|rs|go|java)$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage
|
||||
if validate_file_path "$file_path"; then
|
||||
# Safe to operate on file
|
||||
process_file "$file_path"
|
||||
fi
|
||||
```
|
||||
|
||||
## Pattern: Finding Project Root
|
||||
|
||||
Locate the project root directory from any file path.
|
||||
|
||||
```bash
|
||||
find_project_root() {
|
||||
local dir="$1"
|
||||
|
||||
# Start from file's directory
|
||||
if [ -f "$dir" ]; then
|
||||
dir=$(dirname "$dir")
|
||||
fi
|
||||
|
||||
# Walk up until finding markers
|
||||
while [ "$dir" != "/" ]; do
|
||||
# Check for project markers
|
||||
if [ -f "$dir/package.json" ] || \
|
||||
[ -f "$dir/Cargo.toml" ] || \
|
||||
[ -f "$dir/go.mod" ] || \
|
||||
[ -d "$dir/.git" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage
|
||||
project_root=$(find_project_root "$file_path")
|
||||
if [ -n "$project_root" ]; then
|
||||
cd "$project_root"
|
||||
npm run build
|
||||
fi
|
||||
```
|
||||
|
||||
## Pattern: Conditional Hook Execution
|
||||
|
||||
Run hook only when certain conditions are met.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
MIN_CHANGES=3
|
||||
TARGET_REPO="backend"
|
||||
|
||||
# Check if should run
|
||||
should_run() {
|
||||
# Count recent edits
|
||||
local edit_count=$(tail -20 ~/.claude/edit-log.txt | wc -l)
|
||||
|
||||
if [ "$edit_count" -lt "$MIN_CHANGES" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if target repo was modified
|
||||
if ! tail -20 ~/.claude/edit-log.txt | grep -q "$TARGET_REPO"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Main execution
|
||||
if ! should_run; then
|
||||
echo '{}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run actual hook logic
|
||||
perform_build_check
|
||||
```
|
||||
|
||||
## Pattern: Rate Limiting
|
||||
|
||||
Prevent hooks from running too frequently.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
RATE_LIMIT_FILE="/tmp/hook-last-run"
|
||||
MIN_INTERVAL=30 # seconds
|
||||
|
||||
# Check if enough time has passed
|
||||
should_run() {
|
||||
if [ ! -f "$RATE_LIMIT_FILE" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local last_run=$(cat "$RATE_LIMIT_FILE")
|
||||
local now=$(date +%s)
|
||||
local elapsed=$((now - last_run))
|
||||
|
||||
if [ "$elapsed" -lt "$MIN_INTERVAL" ]; then
|
||||
echo "Skipping (ran ${elapsed}s ago, min interval ${MIN_INTERVAL}s)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Update last run time
|
||||
mark_run() {
|
||||
date +%s > "$RATE_LIMIT_FILE"
|
||||
}
|
||||
|
||||
# Usage
|
||||
if should_run; then
|
||||
perform_expensive_operation
|
||||
mark_run
|
||||
fi
|
||||
|
||||
echo '{}'
|
||||
```
|
||||
|
||||
## Pattern: Multi-Project Detection
|
||||
|
||||
Detect which project/repo a file belongs to.
|
||||
|
||||
```bash
|
||||
detect_project() {
|
||||
local file="$1"
|
||||
local project_root="/Users/myuser/projects"
|
||||
|
||||
# Extract project name from path
|
||||
if [[ "$file" =~ $project_root/([^/]+) ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "unknown"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage
|
||||
project=$(detect_project "$file_path")
|
||||
|
||||
case "$project" in
|
||||
"frontend")
|
||||
npm --prefix ~/projects/frontend run build
|
||||
;;
|
||||
"backend")
|
||||
cargo build --manifest-path ~/projects/backend/Cargo.toml
|
||||
;;
|
||||
*)
|
||||
echo "Unknown project: $project"
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
## Pattern: Graceful Degradation
|
||||
|
||||
Handle failures gracefully without blocking workflow.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Try operation with fallback
|
||||
try_with_fallback() {
|
||||
local primary_cmd="$1"
|
||||
local fallback_cmd="$2"
|
||||
local description="$3"
|
||||
|
||||
echo "Attempting: $description"
|
||||
|
||||
# Try primary command
|
||||
if eval "$primary_cmd" 2>/dev/null; then
|
||||
echo "✅ Success"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "⚠️ Primary failed, trying fallback..."
|
||||
|
||||
# Try fallback
|
||||
if eval "$fallback_cmd" 2>/dev/null; then
|
||||
echo "✅ Fallback succeeded"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "❌ Both failed, continuing anyway"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage
|
||||
try_with_fallback \
|
||||
"npm run build" \
|
||||
"npm run build:dev" \
|
||||
"Building project"
|
||||
|
||||
# Always return empty response (non-blocking)
|
||||
echo '{}'
|
||||
```
|
||||
|
||||
## Pattern: Parallel Execution
|
||||
|
||||
Run multiple checks in parallel for speed.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Run checks in parallel
|
||||
run_parallel_checks() {
|
||||
local pids=()
|
||||
|
||||
# Start each check in background
|
||||
check_typescript &
|
||||
pids+=($!)
|
||||
|
||||
check_eslint &
|
||||
pids+=($!)
|
||||
|
||||
check_tests &
|
||||
pids+=($!)
|
||||
|
||||
# Wait for all to complete
|
||||
local exit_code=0
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" || exit_code=1
|
||||
done
|
||||
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
check_typescript() {
|
||||
npx tsc --noEmit > /tmp/tsc-output.txt 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "TypeScript errors found"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_eslint() {
|
||||
npx eslint . > /tmp/eslint-output.txt 2>&1
|
||||
}
|
||||
|
||||
check_tests() {
|
||||
npm test > /tmp/test-output.txt 2>&1
|
||||
}
|
||||
|
||||
# Usage
|
||||
if run_parallel_checks; then
|
||||
echo "✅ All checks passed"
|
||||
else
|
||||
echo "⚠️ Some checks failed"
|
||||
cat /tmp/tsc-output.txt
|
||||
cat /tmp/eslint-output.txt
|
||||
fi
|
||||
|
||||
echo '{}'
|
||||
```
|
||||
|
||||
## Pattern: Smart Caching
|
||||
|
||||
Cache results to avoid redundant work.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
CACHE_DIR="$HOME/.claude/hook-cache"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
|
||||
# Generate cache key
|
||||
cache_key() {
|
||||
local file="$1"
|
||||
echo -n "$file:$(stat -f %m "$file" 2>/dev/null || stat -c %Y "$file")" | md5sum | cut -d' ' -f1
|
||||
}
|
||||
|
||||
# Check cache
|
||||
check_cache() {
|
||||
local file="$1"
|
||||
local key=$(cache_key "$file")
|
||||
local cache_file="$CACHE_DIR/$key"
|
||||
|
||||
if [ -f "$cache_file" ]; then
|
||||
# Cache hit
|
||||
cat "$cache_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Update cache
|
||||
update_cache() {
|
||||
local file="$1"
|
||||
local result="$2"
|
||||
local key=$(cache_key "$file")
|
||||
local cache_file="$CACHE_DIR/$key"
|
||||
|
||||
echo "$result" > "$cache_file"
|
||||
|
||||
# Clean old cache entries (older than 1 day)
|
||||
find "$CACHE_DIR" -type f -mtime +1 -delete 2>/dev/null
|
||||
}
|
||||
|
||||
# Usage
|
||||
if cached=$(check_cache "$file_path"); then
|
||||
echo "Cache hit: $cached"
|
||||
else
|
||||
result=$(expensive_operation "$file_path")
|
||||
update_cache "$file_path" "$result"
|
||||
echo "Computed: $result"
|
||||
fi
|
||||
```
|
||||
|
||||
## Pattern: Progressive Output
|
||||
|
||||
Show progress for long-running hooks.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Progress indicator
|
||||
show_progress() {
|
||||
local message="$1"
|
||||
echo -n "$message..."
|
||||
}
|
||||
|
||||
complete_progress() {
|
||||
local status="$1"
|
||||
if [ "$status" == "success" ]; then
|
||||
echo " ✅"
|
||||
else
|
||||
echo " ❌"
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage
|
||||
show_progress "Running TypeScript compiler"
|
||||
if npx tsc --noEmit 2>/dev/null; then
|
||||
complete_progress "success"
|
||||
else
|
||||
complete_progress "failure"
|
||||
fi
|
||||
|
||||
show_progress "Running linter"
|
||||
if npx eslint . 2>/dev/null; then
|
||||
complete_progress "success"
|
||||
else
|
||||
complete_progress "failure"
|
||||
fi
|
||||
|
||||
echo '{}'
|
||||
```
|
||||
|
||||
## Pattern: Context Injection
|
||||
|
||||
Inject helpful context into Claude's prompt.
|
||||
|
||||
```javascript
|
||||
// UserPromptSubmit hook
|
||||
function injectContext(prompt) {
|
||||
const context = [];
|
||||
|
||||
// Add relevant documentation
|
||||
if (prompt.includes('API')) {
|
||||
context.push('📖 API Documentation: https://docs.example.com/api');
|
||||
}
|
||||
|
||||
// Add recent changes
|
||||
const recentFiles = getRecentlyEditedFiles();
|
||||
if (recentFiles.length > 0) {
|
||||
context.push(`📝 Recently edited: ${recentFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
// Add project status
|
||||
const buildStatus = getLastBuildStatus();
|
||||
if (!buildStatus.passed) {
|
||||
context.push(`⚠️ Current build has ${buildStatus.errorCount} errors`);
|
||||
}
|
||||
|
||||
if (context.length === 0) {
|
||||
return { decision: 'approve' };
|
||||
}
|
||||
|
||||
return {
|
||||
decision: 'approve',
|
||||
additionalContext: `\n\n---\n${context.join('\n')}\n---\n`
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern: Error Accumulation
|
||||
|
||||
Collect multiple errors before reporting.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
ERRORS=()
|
||||
|
||||
# Add error to collection
|
||||
add_error() {
|
||||
ERRORS+=("$1")
|
||||
}
|
||||
|
||||
# Report all errors
|
||||
report_errors() {
|
||||
if [ ${#ERRORS[@]} -eq 0 ]; then
|
||||
echo "✅ No errors found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "⚠️ Found ${#ERRORS[@]} issue(s):"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
local i=1
|
||||
for error in "${ERRORS[@]}"; do
|
||||
echo "$i. $error"
|
||||
((i++))
|
||||
done
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage
|
||||
if ! run_typescript_check; then
|
||||
add_error "TypeScript compilation failed"
|
||||
fi
|
||||
|
||||
if ! run_lint_check; then
|
||||
add_error "Linting issues found"
|
||||
fi
|
||||
|
||||
if ! run_test_check; then
|
||||
add_error "Tests failing"
|
||||
fi
|
||||
|
||||
report_errors
|
||||
|
||||
echo '{}'
|
||||
```
|
||||
|
||||
## Pattern: Conditional Blocking
|
||||
|
||||
Block only on critical errors, warn on others.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
ERROR_LEVEL="none" # none, warning, critical
|
||||
|
||||
# Check for issues
|
||||
check_critical_issues() {
|
||||
if grep -q "FIXME\|XXX\|TODO: CRITICAL" "$file_path"; then
|
||||
ERROR_LEVEL="critical"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
check_warnings() {
|
||||
if grep -q "console.log\|debugger" "$file_path"; then
|
||||
ERROR_LEVEL="warning"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Run checks
|
||||
check_critical_issues
|
||||
check_warnings
|
||||
|
||||
# Return appropriate decision
|
||||
case "$ERROR_LEVEL" in
|
||||
"critical")
|
||||
echo '{
|
||||
"decision": "block",
|
||||
"reason": "🚫 CRITICAL: Found critical TODOs or FIXMEs that must be addressed"
|
||||
}' | jq -c '.'
|
||||
;;
|
||||
"warning")
|
||||
echo "⚠️ Warning: Found debug statements (console.log, debugger)"
|
||||
echo '{}'
|
||||
;;
|
||||
*)
|
||||
echo '{}'
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
## Pattern: Hook Coordination
|
||||
|
||||
Coordinate between multiple hooks using shared state.
|
||||
|
||||
```bash
|
||||
# Hook 1: Track state
|
||||
#!/bin/bash
|
||||
STATE_FILE="/tmp/hook-state.json"
|
||||
|
||||
# Update state
|
||||
jq -n \
|
||||
--arg timestamp "$(date +%s)" \
|
||||
--arg files "$files_edited" \
|
||||
'{lastRun: $timestamp, filesEdited: ($files | split(","))}' \
|
||||
> "$STATE_FILE"
|
||||
|
||||
echo '{}'
|
||||
```
|
||||
|
||||
```bash
|
||||
# Hook 2: Read state
|
||||
#!/bin/bash
|
||||
STATE_FILE="/tmp/hook-state.json"
|
||||
|
||||
if [ -f "$STATE_FILE" ]; then
|
||||
last_run=$(jq -r '.lastRun' "$STATE_FILE")
|
||||
files=$(jq -r '.filesEdited[]' "$STATE_FILE")
|
||||
|
||||
# Use state from previous hook
|
||||
for file in $files; do
|
||||
process_file "$file"
|
||||
done
|
||||
fi
|
||||
|
||||
echo '{}'
|
||||
```
|
||||
|
||||
## Pattern: User Notification
|
||||
|
||||
Notify user of important events without blocking.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Send desktop notification (macOS)
|
||||
notify_macos() {
|
||||
osascript -e "display notification \"$1\" with title \"Claude Code Hook\""
|
||||
}
|
||||
|
||||
# Send desktop notification (Linux)
|
||||
notify_linux() {
|
||||
notify-send "Claude Code Hook" "$1"
|
||||
}
|
||||
|
||||
# Notify based on OS
|
||||
notify() {
|
||||
local message="$1"
|
||||
|
||||
case "$OSTYPE" in
|
||||
darwin*)
|
||||
notify_macos "$message"
|
||||
;;
|
||||
linux*)
|
||||
notify_linux "$message"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Usage
|
||||
if [ "$error_count" -gt 10 ]; then
|
||||
notify "⚠️ Build has $error_count errors"
|
||||
fi
|
||||
|
||||
echo '{}'
|
||||
```
|
||||
|
||||
## Remember
|
||||
|
||||
- **Keep it simple** - Start with basic patterns, add complexity only when needed
|
||||
- **Test thoroughly** - Test each pattern in isolation before combining
|
||||
- **Fail gracefully** - Non-blocking hooks should never crash workflow
|
||||
- **Log everything** - You'll need it for debugging
|
||||
- **Document patterns** - Future you will thank present you
|
||||
Reference in New Issue
Block a user