Claude Code Hooks Troubleshooting
Tags: troubleshooting debugging error-handling diagnostics
A comprehensive guide to debugging and resolving common hook issues.
Quick Diagnostics
1. Check If Hooks Are Loading
# In Claude Code
/hooks
# Should show your configured hooks
# If empty, check settings.json location2. Verify Configuration File
# Check file exists
ls -la .claude/settings.json
# Validate JSON syntax
jq . .claude/settings.json
# Check for syntax errors
python -m json.tool .claude/settings.json3. Test Basic Hook
{
"hooks": {
"PostToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "echo 'Hook test successful' >> /tmp/claude-hook-test.txt"
}
]
}
]
}
}Common Issues and Solutions
Issue: Hook Not Triggering
Symptoms:
- No output from hooks
- Expected behavior doesn’t occur
- No error messages
Solutions:
- Check Matcher Pattern:
# Test your regex
echo "Edit" | grep -E "Edit|Write"
# Common matcher mistakes
"Edit|Write" # Correct
"Edit\|Write" # Incorrect (unless you want literal |)
"Edit OR Write" # Incorrect- Verify Event Name:
{
"hooks": {
"PreToolUse": [], // Correct
"pretooluse": [], // Incorrect (case sensitive)
"PreTool": [] // Incorrect (wrong name)
}
}- Check File Location:
# Claude checks these locations in order:
1. ./.claude/settings.json # Project-specific
2. ~/.claude/settings.json # User-global
3. ./.claude/settings.local.json # Local overridesIssue: JSON Parsing Errors
Symptoms:
- “Invalid JSON” errors
- Hooks fail silently
- Unexpected behavior
Solutions:
- Validate JSON Structure:
# Common JSON errors
{
"hooks": {
"PreToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "echo test" # Missing comma here
}
{ # This object will cause error
"type": "command",
"command": "echo test2"
}
]
}
]
}
}- Safe JSON Parsing in Hooks:
#!/bin/bash
# Read stdin safely
JSON_INPUT=$(cat)
# Validate before parsing
if ! echo "$JSON_INPUT" | jq empty 2>/dev/null; then
echo "Error: Invalid JSON received" >&2
exit 1
fi
# Now safe to use
TOOL_NAME=$(echo "$JSON_INPUT" | jq -r '.tool_name // "unknown"')Issue: Exit Code Problems
Symptoms:
- Operations not blocked when they should be
- Hooks blocking unintentionally
- Confusing behavior
Solutions:
- Understanding Exit Codes:
#!/bin/bash
# Exit code reference
# Success - continue normally
exit 0
# Block operation (PreToolUse only)
exit 2
# Warning - log but continue
exit 1
exit 3
exit 255
# Any non-zero except 2- Testing Exit Codes:
#!/bin/bash
# Test different scenarios
case "$1" in
"allow")
echo "Allowing operation"
exit 0
;;
"block")
echo "Blocking operation" >&2
exit 2
;;
"warn")
echo "Warning but continuing" >&2
exit 1
;;
esacIssue: Performance Problems
Symptoms:
- Claude Code becomes slow
- Hooks timeout
- Operations hang
Solutions:
- Add Timeouts:
{
"hooks": {
"PostToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "timeout 5s my-slow-command || true",
"timeout": 10000 // 10 second timeout
}
]
}
]
}
}- Profile Hook Performance:
#!/bin/bash
# Performance profiling wrapper
SCRIPT_TO_PROFILE="$1"
START=$(date +%s.%N)
# Run the actual hook
$SCRIPT_TO_PROFILE
RESULT=$?
END=$(date +%s.%N)
DURATION=$(echo "$END - $START" | bc)
# Log if slow
if (( $(echo "$DURATION > 1.0" | bc -l) )); then
echo "[SLOW HOOK] $SCRIPT_TO_PROFILE took ${DURATION}s" >> ~/.claude/performance.log
fi
exit $RESULT- Use Background Processing:
#!/bin/bash
# For non-critical long operations
# Start in background
{
sleep 10
curl -X POST https://api.example.com/notify
} &
# Return immediately
exit 0Issue: Environment Problems
Symptoms:
- Commands not found
- Different behavior than manual testing
- Path issues
Solutions:
- Debug Environment:
#!/bin/bash
# Save as debug_env.sh
{
echo "=== Environment Debug ==="
echo "PATH: $PATH"
echo "PWD: $PWD"
echo "USER: $USER"
echo "SHELL: $SHELL"
echo ""
echo "=== Claude Variables ==="
env | grep CLAUDE
echo ""
echo "=== Available Commands ==="
for cmd in jq git npm python3 node curl; do
if command -v $cmd &> /dev/null; then
echo "✓ $cmd: $(command -v $cmd)"
else
echo "✗ $cmd: NOT FOUND"
fi
done
} >> ~/.claude/env-debug.log- Fix PATH Issues:
#!/bin/bash
# Add common paths
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"
# For macOS with Homebrew
export PATH="/opt/homebrew/bin:$PATH"
# For custom tools
export PATH="$HOME/.local/bin:$PATH"Issue: File Access Problems
Symptoms:
- Permission denied errors
- File not found errors
- Unexpected file paths
Solutions:
- Handle Spaces in Paths:
# WRONG
cat $CLAUDE_FILE_PATHS
# CORRECT
for file in $CLAUDE_FILE_PATHS; do
cat "$file" # Quote the variable
done
# BETTER
IFS=' ' read -ra FILES <<< "$CLAUDE_FILE_PATHS"
for file in "${FILES[@]}"; do
cat "$file"
done- Check File Existence:
#!/bin/bash
for file in $CLAUDE_FILE_PATHS; do
if [ ! -f "$file" ]; then
echo "Error: File not found: $file" >&2
continue
fi
if [ ! -r "$file" ]; then
echo "Error: Cannot read file: $file" >&2
continue
fi
# Process file
process_file "$file"
doneAdvanced Debugging Techniques
1. Comprehensive Logging
#!/usr/bin/env python3
"""
Advanced debugging hook
Save as: .claude/hooks/debug_hook.py
"""
import json
import sys
import os
import traceback
import logging
from datetime import datetime
from pathlib import Path
# Setup detailed logging
log_dir = Path.home() / '.claude' / 'logs'
log_dir.mkdir(exist_ok=True)
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_dir / 'hooks_debug.log'),
logging.StreamHandler(sys.stderr)
]
)
logger = logging.getLogger('HookDebug')
def main():
logger.info("="*50)
logger.info("Hook execution started")
try:
# Log environment
logger.debug("Environment variables:")
for key, value in os.environ.items():
if key.startswith('CLAUDE'):
logger.debug(f" {key}={value}")
# Read and log input
raw_input = sys.stdin.read()
logger.debug(f"Raw input: {raw_input}")
# Parse JSON
data = json.loads(raw_input)
logger.info(f"Parsed data: {json.dumps(data, indent=2)}")
# Your hook logic here
result = process_hook(data)
logger.info("Hook completed successfully")
return result
except json.JSONDecodeError as e:
logger.error(f"JSON parsing failed: {e}")
logger.error(f"Input was: {raw_input[:200]}...")
return 1
except Exception as e:
logger.error(f"Unexpected error: {e}")
logger.error(traceback.format_exc())
return 1
finally:
logger.info("Hook execution ended")
logger.info("="*50)
def process_hook(data):
# Your actual hook logic
return 0
if __name__ == '__main__':
sys.exit(main())2. Interactive Debugging
#!/bin/bash
# Interactive debug mode
# Save as: .claude/hooks/interactive_debug.sh
if [ "$CLAUDE_DEBUG" = "true" ]; then
echo "🔍 DEBUG MODE ACTIVATED" >&2
echo "Press Ctrl+C to abort, Enter to continue" >&2
read -r
# Show current state
echo "Current State:" >&2
echo " Tool: $CLAUDE_TOOL_NAME" >&2
echo " Files: $CLAUDE_FILE_PATHS" >&2
echo " PWD: $(pwd)" >&2
# Start interactive shell for debugging
echo "Starting debug shell. Type 'exit' to continue hook." >&2
bash -i
fi
# Normal hook execution continues here3. Hook Testing Framework
#!/usr/bin/env python3
"""
Test your hooks before deployment
Save as: test_hooks.py
"""
import subprocess
import json
import tempfile
from pathlib import Path
def test_hook(hook_path, test_cases):
"""Test a hook with various inputs"""
results = []
for test in test_cases:
print(f"\nTesting: {test['name']}")
print(f"Input: {json.dumps(test['input'], indent=2)}")
# Run hook
proc = subprocess.run(
['bash', hook_path] if hook_path.endswith('.sh') else ['python3', hook_path],
input=json.dumps(test['input']),
capture_output=True,
text=True
)
# Check result
success = proc.returncode == test.get('expected_exit', 0)
print(f"Exit code: {proc.returncode} (expected: {test.get('expected_exit', 0)})")
print(f"Stdout: {proc.stdout}")
print(f"Stderr: {proc.stderr}")
print(f"Result: {'✅ PASS' if success else '❌ FAIL'}")
results.append({
'test': test['name'],
'success': success,
'exit_code': proc.returncode,
'stdout': proc.stdout,
'stderr': proc.stderr
})
return results
# Example usage
if __name__ == '__main__':
test_cases = [
{
'name': 'Normal file edit',
'input': {
'hook_event_name': 'PreToolUse',
'tool_name': 'Edit',
'tool_input': {'file_path': '/tmp/test.txt'}
},
'expected_exit': 0
},
{
'name': 'Blocked file edit',
'input': {
'hook_event_name': 'PreToolUse',
'tool_name': 'Edit',
'tool_input': {'file_path': '.env'}
},
'expected_exit': 2
}
]
results = test_hook('.claude/hooks/security_check.sh', test_cases)
# Summary
passed = sum(1 for r in results if r['success'])
print(f"\n{'='*50}")
print(f"Tests passed: {passed}/{len(results)}")Debugging Checklist
Before Writing Hooks
- Understand the hook event lifecycle
- Know which events can block operations
- Plan error handling strategy
- Set up logging infrastructure
During Development
- Start with echo/print statements
- Test hooks outside Claude first
- Use meaningful error messages
- Add timing information
- Handle edge cases
When Issues Occur
- Check hook is in correct location
- Validate JSON syntax
- Verify matcher patterns
- Test with simple echo hook
- Check file permissions
- Review environment variables
- Look for timeout issues
- Check exit codes
Production Monitoring
- Set up log rotation
- Monitor hook performance
- Track error rates
- Set up alerts for failures
- Regular hook audits
Emergency Procedures
Disable All Hooks
# Temporarily rename settings
mv .claude/settings.json .claude/settings.json.disabled
# Or use empty configuration
echo '{"hooks": {}}' > .claude/settings.jsonDebug Mode
# Enable debug mode for all hooks
export CLAUDE_DEBUG=true
claude-codeRecovery Steps
- Disable problematic hooks
- Review recent changes
- Check system resources
- Restart Claude Code
- Test with minimal configuration
- Gradually re-enable hooks
Getting Help
Useful Commands
# View Claude Code version
claude-code --version
# Check current configuration
cat .claude/settings.json | jq .
# Find all hook files
find . -name "*.sh" -o -name "*.py" | grep -E "(hook|claude)"
# Search hook logs
grep -r "ERROR\|FAIL" ~/.claude/logs/Resources
- GitHub Issues: Report bugs
- Documentation:
/helpin Claude Code - Community Forums: Share solutions
- This guide: Keep it handy!
Remember: Most hook issues are configuration or environment related. Start with simple tests and build up complexity gradually.