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
| 1 | # Advanced Hook Use Cases |
| 2 | |
| 3 | This reference covers advanced hook patterns and techniques for sophisticated automation workflows. |
| 4 | |
| 5 | ## Multi-Stage Validation |
| 6 | |
| 7 | Combine command and prompt hooks for layered validation: |
| 8 | |
| 9 | ```json |
| 10 | { |
| 11 | "PreToolUse": [ |
| 12 | { |
| 13 | "matcher": "Bash", |
| 14 | "hooks": [ |
| 15 | { |
| 16 | "type": "command", |
| 17 | "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/quick-check.sh", |
| 18 | "timeout": 5 |
| 19 | }, |
| 20 | { |
| 21 | "type": "prompt", |
| 22 | "prompt": "Deep analysis of bash command: $TOOL_INPUT", |
| 23 | "timeout": 15 |
| 24 | } |
| 25 | ] |
| 26 | } |
| 27 | ] |
| 28 | } |
| 29 | ``` |
| 30 | |
| 31 | **Use case:** Fast deterministic checks followed by intelligent analysis |
| 32 | |
| 33 | **Example quick-check.sh:** |
| 34 | ```bash |
| 35 | #!/bin/bash |
| 36 | input=$(cat) |
| 37 | command=$(echo "$input" | jq -r '.tool_input.command') |
| 38 | |
| 39 | # Immediate approval for safe commands |
| 40 | if [[ "$command" =~ ^(ls|pwd|echo|date|whoami)$ ]]; then |
| 41 | exit 0 |
| 42 | fi |
| 43 | |
| 44 | # Let prompt hook handle complex cases |
| 45 | exit 0 |
| 46 | ``` |
| 47 | |
| 48 | The command hook quickly approves obviously safe commands, while the prompt hook analyzes everything else. |
| 49 | |
| 50 | ## Conditional Hook Execution |
| 51 | |
| 52 | Execute hooks based on environment or context: |
| 53 | |
| 54 | ```bash |
| 55 | #!/bin/bash |
| 56 | # Only run in CI environment |
| 57 | if [ -z "$CI" ]; then |
| 58 | echo '{"continue": true}' # Skip in non-CI |
| 59 | exit 0 |
| 60 | fi |
| 61 | |
| 62 | # Run validation logic in CI |
| 63 | input=$(cat) |
| 64 | # ... validation code ... |
| 65 | ``` |
| 66 | |
| 67 | **Use cases:** |
| 68 | - Different behavior in CI vs local development |
| 69 | - Project-specific validation |
| 70 | - User-specific rules |
| 71 | |
| 72 | **Example: Skip certain checks for trusted users:** |
| 73 | ```bash |
| 74 | #!/bin/bash |
| 75 | # Skip detailed checks for admin users |
| 76 | if [ "$USER" = "admin" ]; then |
| 77 | exit 0 |
| 78 | fi |
| 79 | |
| 80 | # Full validation for other users |
| 81 | input=$(cat) |
| 82 | # ... validation code ... |
| 83 | ``` |
| 84 | |
| 85 | ## Hook Chaining via State |
| 86 | |
| 87 | Share state between hooks using temporary files: |
| 88 | |
| 89 | ```bash |
| 90 | # Hook 1: Analyze and save state |
| 91 | #!/bin/bash |
| 92 | input=$(cat) |
| 93 | command=$(echo "$input" | jq -r '.tool_input.command') |
| 94 | |
| 95 | # Analyze command |
| 96 | risk_level=$(calculate_risk "$command") |
| 97 | echo "$risk_level" > /tmp/hook-state-$$ |
| 98 | |
| 99 | exit 0 |
| 100 | ``` |
| 101 | |
| 102 | ```bash |
| 103 | # Hook 2: Use saved state |
| 104 | #!/bin/bash |
| 105 | risk_level=$(cat /tmp/hook-state-$$ 2>/dev/null || echo "unknown") |
| 106 | |
| 107 | if [ "$risk_level" = "high" ]; then |
| 108 | echo "High risk operation detected" >&2 |
| 109 | exit 2 |
| 110 | fi |
| 111 | ``` |
| 112 | |
| 113 | **Important:** This only works for sequential hook events (e.g., PreToolUse then PostToolUse), not parallel hooks. |
| 114 | |
| 115 | ## Dynamic Hook Configuration |
| 116 | |
| 117 | Modify hook behavior based on project configuration: |
| 118 | |
| 119 | ```bash |
| 120 | #!/bin/bash |
| 121 | cd "$CLAUDE_PROJECT_DIR" || exit 1 |
| 122 | |
| 123 | # Read project-specific config |
| 124 | if [ -f ".claude-hooks-config.json" ]; then |
| 125 | strict_mode=$(jq -r '.strict_mode' .claude-hooks-config.json) |
| 126 | |
| 127 | if [ "$strict_mode" = "true" ]; then |
| 128 | # Apply strict validation |
| 129 | # ... |
| 130 | else |
| 131 | # Apply lenient validation |
| 132 | # ... |
| 133 | fi |
| 134 | fi |
| 135 | ``` |
| 136 | |
| 137 | **Example .claude-hooks-config.json:** |
| 138 | ```json |
| 139 | { |
| 140 | "strict_mode": true, |
| 141 | "allowed_commands": ["ls", "pwd", "grep"], |
| 142 | "forbidden_paths": ["/etc", "/sys"] |
| 143 | } |
| 144 | ``` |
| 145 | |
| 146 | ## Context-Aware Prompt Hooks |
| 147 | |
| 148 | Use transcript and session context for intelligent decisions: |
| 149 | |
| 150 | ```json |
| 151 | { |
| 152 | "Stop": [ |
| 153 | { |
| 154 | "matcher": "*", |
| 155 | "hooks": [ |
| 156 | { |
| 157 | "type": "prompt", |
| 158 | "prompt": "Review the full transcript at $TRANSCRIPT_PATH. Check: 1) Were tests run after code changes? 2) Did the build succeed? 3) Were all user questions answered? 4) Is there any unfinished work? Return 'approve' only if everything is complete." |
| 159 | } |
| 160 | ] |
| 161 | } |
| 162 | ] |
| 163 | } |
| 164 | ``` |
| 165 | |
| 166 | The LLM can read the transcript file and make context-aware decisions. |
| 167 | |
| 168 | ## Performance Optimization |
| 169 | |
| 170 | ### Caching Validation Results |
| 171 | |
| 172 | ```bash |
| 173 | #!/bin/bash |
| 174 | input=$(cat) |
| 175 | file_path=$(echo "$input" | jq -r '.tool_input.file_path') |
| 176 | cache_key=$(echo -n "$file_path" | md5sum | cut -d' ' -f1) |
| 177 | cache_file="/tmp/hook-cache-$cache_key" |
| 178 | |
| 179 | # Check cache |
| 180 | if [ -f "$cache_file" ]; then |
| 181 | cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || stat -c%Y "$cache_file"))) |
| 182 | if [ "$cache_age" -lt 300 ]; then # 5 minute cache |
| 183 | cat "$cache_file" |
| 184 | exit 0 |
| 185 | fi |
| 186 | fi |
| 187 | |
| 188 | # Perform validation |
| 189 | result='{"decision": "approve"}' |
| 190 | |
| 191 | # Cache result |
| 192 | echo "$result" > "$cache_file" |
| 193 | echo "$result" |
| 194 | ``` |
| 195 | |
| 196 | ### Parallel Execution Optimization |
| 197 | |
| 198 | Since hooks run in parallel, design them to be independent: |
| 199 | |
| 200 | ```json |
| 201 | { |
| 202 | "PreToolUse": [ |
| 203 | { |
| 204 | "matcher": "Write", |
| 205 | "hooks": [ |
| 206 | { |
| 207 | "type": "command", |
| 208 | "command": "bash check-size.sh", // Independent |
| 209 | "timeout": 2 |
| 210 | }, |
| 211 | { |
| 212 | "type": "command", |
| 213 | "command": "bash check-path.sh", // Independent |
| 214 | "timeout": 2 |
| 215 | }, |
| 216 | { |
| 217 | "type": "prompt", |
| 218 | "prompt": "Check content safety", // Independent |
| 219 | "timeout": 10 |
| 220 | } |
| 221 | ] |
| 222 | } |
| 223 | ] |
| 224 | } |
| 225 | ``` |
| 226 | |
| 227 | All three hooks run simultaneously, reducing total latency. |
| 228 | |
| 229 | ## Cross-Event Workflows |
| 230 | |
| 231 | Coordinate hooks across different events: |
| 232 | |
| 233 | **SessionStart - Set up tracking:** |
| 234 | ```bash |
| 235 | #!/bin/bash |
| 236 | # Initialize session tracking |
| 237 | echo "0" > /tmp/test-count-$$ |
| 238 | echo "0" > /tmp/build-count-$$ |
| 239 | ``` |
| 240 | |
| 241 | **PostToolUse - Track events:** |
| 242 | ```bash |
| 243 | #!/bin/bash |
| 244 | input=$(cat) |
| 245 | tool_name=$(echo "$input" | jq -r '.tool_name') |
| 246 | |
| 247 | if [ "$tool_name" = "Bash" ]; then |
| 248 | command=$(echo "$input" | jq -r '.tool_result') |
| 249 | if [[ "$command" == *"test"* ]]; then |
| 250 | count=$(cat /tmp/test-count-$$ 2>/dev/null || echo "0") |
| 251 | echo $((count + 1)) > /tmp/test-count-$$ |
| 252 | fi |
| 253 | fi |
| 254 | ``` |
| 255 | |
| 256 | **Stop - Verify based on tracking:** |
| 257 | ```bash |
| 258 | #!/bin/bash |
| 259 | test_count=$(cat /tmp/test-count-$$ 2>/dev/null || echo "0") |
| 260 | |
| 261 | if [ "$test_count" -eq 0 ]; then |
| 262 | echo '{"decision": "block", "reason": "No tests were run"}' >&2 |
| 263 | exit 2 |
| 264 | fi |
| 265 | ``` |
| 266 | |
| 267 | ## Integration with External Systems |
| 268 | |
| 269 | ### Slack Notifications |
| 270 | |
| 271 | ```bash |
| 272 | #!/bin/bash |
| 273 | input=$(cat) |
| 274 | tool_name=$(echo "$input" | jq -r '.tool_name') |
| 275 | decision="blocked" |
| 276 | |
| 277 | # Send notification to Slack |
| 278 | curl -X POST "$SLACK_WEBHOOK" \ |
| 279 | -H 'Content-Type: application/json' \ |
| 280 | -d "{\"text\": \"Hook ${decision} ${tool_name} operation\"}" \ |
| 281 | 2>/dev/null |
| 282 | |
| 283 | echo '{"decision": "deny"}' >&2 |
| 284 | exit 2 |
| 285 | ``` |
| 286 | |
| 287 | ### Database Logging |
| 288 | |
| 289 | ```bash |
| 290 | #!/bin/bash |
| 291 | input=$(cat) |
| 292 | |
| 293 | # Log to database |
| 294 | psql "$DATABASE_URL" -c "INSERT INTO hook_logs (event, data) VALUES ('PreToolUse', '$input')" \ |
| 295 | 2>/dev/null |
| 296 | |
| 297 | exit 0 |
| 298 | ``` |
| 299 | |
| 300 | ### Metrics Collection |
| 301 | |
| 302 | ```bash |
| 303 | #!/bin/bash |
| 304 | input=$(cat) |
| 305 | tool_name=$(echo "$input" | jq -r '.tool_name') |
| 306 | |
| 307 | # Send metrics to monitoring system |
| 308 | echo "hook.pretooluse.${tool_name}:1|c" | nc -u -w1 statsd.local 8125 |
| 309 | |
| 310 | exit 0 |
| 311 | ``` |
| 312 | |
| 313 | ## Security Patterns |
| 314 | |
| 315 | ### Rate Limiting |
| 316 | |
| 317 | ```bash |
| 318 | #!/bin/bash |
| 319 | input=$(cat) |
| 320 | command=$(echo "$input" | jq -r '.tool_input.command') |
| 321 | |
| 322 | # Track command frequency |
| 323 | rate_file="/tmp/hook-rate-$$" |
| 324 | current_minute=$(date +%Y%m%d%H%M) |
| 325 | |
| 326 | if [ -f "$rate_file" ]; then |
| 327 | last_minute=$(head -1 "$rate_file") |
| 328 | count=$(tail -1 "$rate_file") |
| 329 | |
| 330 | if [ "$current_minute" = "$last_minute" ]; then |
| 331 | if [ "$count" -gt 10 ]; then |
| 332 | echo '{"decision": "deny", "reason": "Rate limit exceeded"}' >&2 |
| 333 | exit 2 |
| 334 | fi |
| 335 | count=$((count + 1)) |
| 336 | else |
| 337 | count=1 |
| 338 | fi |
| 339 | else |
| 340 | count=1 |
| 341 | fi |
| 342 | |
| 343 | echo "$current_minute" > "$rate_file" |
| 344 | echo "$count" >> "$rate_file" |
| 345 | |
| 346 | exit 0 |
| 347 | ``` |
| 348 | |
| 349 | ### Audit Logging |
| 350 | |
| 351 | ```bash |
| 352 | #!/bin/bash |
| 353 | input=$(cat) |
| 354 | tool_name=$(echo "$input" | jq -r '.tool_name') |
| 355 | timestamp=$(date -Iseconds) |
| 356 | |
| 357 | # Append to audit log |
| 358 | echo "$timestamp | $USER | $tool_name | $input" >> ~/.claude/audit.log |
| 359 | |
| 360 | exit 0 |
| 361 | ``` |
| 362 | |
| 363 | ### Secret Detection |
| 364 | |
| 365 | ```bash |
| 366 | #!/bin/bash |
| 367 | input=$(cat) |
| 368 | content=$(echo "$input" | jq -r '.tool_input.content') |
| 369 | |
| 370 | # Check for common secret patterns |
| 371 | if echo "$content" | grep -qE "(api[_-]?key|password|secret|token).{0,20}['\"]?[A-Za-z0-9]{20,}"; then |
| 372 | echo '{"decision": "deny", "reason": "Potential secret detected in content"}' >&2 |
| 373 | exit 2 |
| 374 | fi |
| 375 | |
| 376 | exit 0 |
| 377 | ``` |
| 378 | |
| 379 | ## Testing Advanced Hooks |
| 380 | |
| 381 | ### Unit Testing Hook Scripts |
| 382 | |
| 383 | ```bash |
| 384 | # test-hook.sh |
| 385 | #!/bin/bash |
| 386 | |
| 387 | # Test 1: Approve safe command |
| 388 | result=$(echo '{"tool_input": {"command": "ls"}}' | bash validate-bash.sh) |
| 389 | if [ $? -eq 0 ]; then |
| 390 | echo "✓ Test 1 passed" |
| 391 | else |
| 392 | echo "✗ Test 1 failed" |
| 393 | fi |
| 394 | |
| 395 | # Test 2: Block dangerous command |
| 396 | result=$(echo '{"tool_input": {"command": "rm -rf /"}}' | bash validate-bash.sh) |
| 397 | if [ $? -eq 2 ]; then |
| 398 | echo "✓ Test 2 passed" |
| 399 | else |
| 400 | echo "✗ Test 2 failed" |
| 401 | fi |
| 402 | ``` |
| 403 | |
| 404 | ### Integration Testing |
| 405 | |
| 406 | Create test scenarios that exercise the full hook workflow: |
| 407 | |
| 408 | ```bash |
| 409 | # integration-test.sh |
| 410 | #!/bin/bash |
| 411 | |
| 412 | # Set up test environment |
| 413 | export CLAUDE_PROJECT_DIR="/tmp/test-project" |
| 414 | export CLAUDE_PLUGIN_ROOT="$(pwd)" |
| 415 | mkdir -p "$CLAUDE_PROJECT_DIR" |
| 416 | |
| 417 | # Test SessionStart hook |
| 418 | echo '{}' | bash hooks/session-start.sh |
| 419 | if [ -f "/tmp/session-initialized" ]; then |
| 420 | echo "✓ SessionStart hook works" |
| 421 | else |
| 422 | echo "✗ SessionStart hook failed" |
| 423 | fi |
| 424 | |
| 425 | # Clean up |
| 426 | rm -rf "$CLAUDE_PROJECT_DIR" |
| 427 | ``` |
| 428 | |
| 429 | ## Best Practices for Advanced Hooks |
| 430 | |
| 431 | 1. **Keep hooks independent**: Don't rely on execution order |
| 432 | 2. **Use timeouts**: Set appropriate limits for each hook type |
| 433 | 3. **Handle errors gracefully**: Provide clear error messages |
| 434 | 4. **Document complexity**: Explain advanced patterns in README |
| 435 | 5. **Test thoroughly**: Cover edge cases and failure modes |
| 436 | 6. **Monitor performance**: Track hook execution time |
| 437 | 7. **Version configuration**: Use version control for hook configs |
| 438 | 8. **Provide escape hatches**: Allow users to bypass hooks when needed |
| 439 | |
| 440 | ## Common Pitfalls |
| 441 | |
| 442 | ### ❌ Assuming Hook Order |
| 443 | |
| 444 | ```bash |
| 445 | # BAD: Assumes hooks run in specific order |
| 446 | # Hook 1 saves state, Hook 2 reads it |
| 447 | # This can fail because hooks run in parallel! |
| 448 | ``` |
| 449 | |
| 450 | ### ❌ Long-Running Hooks |
| 451 | |
| 452 | ```bash |
| 453 | # BAD: Hook takes 2 minutes to run |
| 454 | sleep 120 |
| 455 | # This will timeout and block the workflow |
| 456 | ``` |
| 457 | |
| 458 | ### ❌ Uncaught Exceptions |
| 459 | |
| 460 | ```bash |
| 461 | # BAD: Script crashes on unexpected input |
| 462 | file_path=$(echo "$input" | jq -r '.tool_input.file_path') |
| 463 | cat "$file_path" # Fails if file doesn't exist |
| 464 | ``` |
| 465 | |
| 466 | ### ✅ Proper Error Handling |
| 467 | |
| 468 | ```bash |
| 469 | # GOOD: Handles errors gracefully |
| 470 | file_path=$(echo "$input" | jq -r '.tool_input.file_path') |
| 471 | if [ ! -f "$file_path" ]; then |
| 472 | echo '{"continue": true, "systemMessage": "File not found, skipping check"}' >&2 |
| 473 | exit 0 |
| 474 | fi |
| 475 | ``` |
| 476 | |
| 477 | ## Conclusion |
| 478 | |
| 479 | Advanced hook patterns enable sophisticated automation while maintaining reliability and performance. Use these techniques when basic hooks are insufficient, but always prioritize simplicity and maintainability. |