9.4 KiB
Troubleshooting
Common issues and solutions when working with hooks.
Hook Not Triggering
Symptom
Hook never executes, even when expected event occurs.
Diagnostic steps
1. Enable debug mode
claude --debug
Look for:
[DEBUG] Getting matching hook commands for PreToolUse with query: Bash
[DEBUG] Found 0 hooks
2. Check hook file location
Hooks must be in:
- Project:
.claude/hooks.json - User:
~/.claude/hooks.json - Plugin:
{plugin}/hooks.json
Verify:
cat .claude/hooks.json
# or
cat ~/.claude/hooks.json
3. Validate JSON syntax
Invalid JSON is silently ignored:
jq . .claude/hooks.json
If error: fix JSON syntax.
4. Check matcher pattern
Common mistakes:
❌ Case sensitivity
{
"matcher": "bash" // Won't match "Bash"
}
✅ Fix
{
"matcher": "Bash"
}
❌ Missing escape for regex
{
"matcher": "mcp__memory__*" // Literal *, not wildcard
}
✅ Fix
{
"matcher": "mcp__memory__.*" // Regex wildcard
}
5. Test matcher in isolation
node -e "console.log(/Bash/.test('Bash'))" # true
node -e "console.log(/bash/.test('Bash'))" # false
Solutions
Missing hook file: Create .claude/hooks.json or ~/.claude/hooks.json
Invalid JSON: Use jq to validate and format:
jq . .claude/hooks.json > temp.json && mv temp.json .claude/hooks.json
Wrong matcher: Check tool names with --debug and update matcher
No matcher specified: If you want to match all tools, omit the matcher field entirely:
{
"hooks": {
"PreToolUse": [
{
"hooks": [...] // No matcher = all tools
}
]
}
}
Command Hook Failing
Symptom
Hook executes but fails with error.
Diagnostic steps
1. Check debug output
[DEBUG] Hook command completed with status 1: <error message>
Status 1 = command failed.
2. Test command directly
Copy the command and run in terminal:
echo '{"session_id":"test","tool_name":"Bash"}' | /path/to/your/hook.sh
3. Check permissions
ls -l /path/to/hook.sh
chmod +x /path/to/hook.sh # If not executable
4. Verify dependencies
Does the command require tools?
which jq # Check if jq is installed
which osascript # macOS only
Common issues
Missing executable permission
chmod +x /path/to/hook.sh
Missing dependencies
Install required tools:
# macOS
brew install jq
# Linux
apt-get install jq
Bad path
Use absolute paths:
{
"command": "/Users/username/.claude/hooks/script.sh"
}
Or use environment variables:
{
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/script.sh"
}
Timeout
If command takes too long:
{
"command": "/path/to/slow-script.sh",
"timeout": 120000 // 2 minutes
}
Prompt Hook Not Working
Symptom
Prompt hook blocks everything or doesn't block when expected.
Diagnostic steps
1. Check LLM response format
Debug output shows:
[DEBUG] Hook command completed with status 0: {"decision": "approve", "reason": "ok"}
Verify JSON is valid.
2. Check prompt structure
Ensure prompt is clear:
{
"prompt": "Evaluate: $ARGUMENTS\n\nReturn JSON: {\"decision\": \"approve\" or \"block\", \"reason\": \"why\"}"
}
3. Test prompt manually
Submit similar prompt to Claude directly to see response format.
Common issues
Ambiguous instructions
❌ Vague
{
"prompt": "Is this ok? $ARGUMENTS"
}
✅ Clear
{
"prompt": "Check if this command is safe: $ARGUMENTS\n\nBlock if: contains 'rm -rf', 'mkfs', or force push to main\n\nReturn: {\"decision\": \"approve\" or \"block\", \"reason\": \"explanation\"}"
}
Missing $ARGUMENTS
❌ No placeholder
{
"prompt": "Validate this command"
}
✅ With placeholder
{
"prompt": "Validate this command: $ARGUMENTS"
}
Invalid JSON response
The LLM must return valid JSON. If it returns plain text, the hook fails.
Add explicit formatting instructions:
IMPORTANT: Return ONLY valid JSON, no other text:
{
"decision": "approve" or "block",
"reason": "your explanation"
}
Hook Blocks Everything
Symptom
Hook blocks all operations, even safe ones.
Diagnostic steps
1. Check hook logic
Review the script/prompt logic. Is the condition too broad?
2. Test with known-safe input
echo '{"tool_name":"Read","tool_input":{"file_path":"test.txt"}}' | /path/to/hook.sh
Expected: {"decision": "approve"}
3. Check for errors in script
Add error output:
#!/bin/bash
set -e # Exit on error
input=$(cat)
# ... rest of script
Solutions
Logic error
Review conditions:
# Before (blocks everything)
if [[ "$command" != "safe_command" ]]; then
block
fi
# After (blocks dangerous commands)
if [[ "$command" == *"dangerous"* ]]; then
block
fi
Default to approve
If logic is complex, default to approve on unclear cases:
# Default
decision="approve"
reason="ok"
# Only change if dangerous
if [[ "$command" == *"rm -rf"* ]]; then
decision="block"
reason="Dangerous command"
fi
echo "{\"decision\": \"$decision\", \"reason\": \"$reason\"}"
Infinite Loop in Stop Hook
Symptom
Stop hook runs repeatedly, Claude never stops.
Cause
Hook blocks stop without checking stop_hook_active flag.
Solution
Always check the flag:
#!/bin/bash
input=$(cat)
stop_hook_active=$(echo "$input" | jq -r '.stop_hook_active')
# If hook already active, don't block again
if [[ "$stop_hook_active" == "true" ]]; then
echo '{"decision": undefined}'
exit 0
fi
# Your logic here
if [ tests_passing ]; then
echo '{"decision": "approve", "reason": "Tests pass"}'
else
echo '{"decision": "block", "reason": "Tests failing"}'
fi
Or in prompt hooks:
{
"prompt": "Evaluate stopping: $ARGUMENTS\n\nIMPORTANT: If stop_hook_active is true, return {\"decision\": undefined}\n\nOtherwise check if tasks complete..."
}
Hook Output Not Visible
Symptom
Hook runs but output not shown to user.
Cause
suppressOutput: true or output goes to stderr.
Solutions
Don't suppress output:
{
"decision": "approve",
"reason": "ok",
"suppressOutput": false
}
Use systemMessage:
{
"decision": "approve",
"reason": "ok",
"systemMessage": "This message will be shown to user"
}
Write to stdout, not stderr:
echo "This is shown" >&1
echo "This is hidden" >&2
Permission Errors
Symptom
Hook script can't read files or execute commands.
Solutions
Make script executable:
chmod +x /path/to/hook.sh
Check file ownership:
ls -l /path/to/hook.sh
chown $USER /path/to/hook.sh
Use absolute paths:
# Instead of
command="./script.sh"
# Use
command="$CLAUDE_PROJECT_DIR/.claude/hooks/script.sh"
Hook Timeouts
Symptom
[DEBUG] Hook command timed out after 60000ms
Solutions
Increase timeout:
{
"type": "command",
"command": "/path/to/slow-script.sh",
"timeout": 300000 // 5 minutes
}
Optimize script:
- Reduce unnecessary operations
- Cache results when possible
- Run expensive operations in background
Run in background:
#!/bin/bash
# Start long operation in background
/path/to/long-operation.sh &
# Return immediately
echo '{"decision": "approve", "reason": "ok"}'
Matcher Conflicts
Symptom
Multiple hooks triggering when only one expected.
Cause
Tool name matches multiple matchers.
Diagnostic
[DEBUG] Matched 3 hooks for query "Bash"
Solutions
Be more specific:
// Instead of
{"matcher": ".*"} // Matches everything
// Use
{"matcher": "Bash"} // Exact match
Check overlapping patterns:
{
"hooks": {
"PreToolUse": [
{"matcher": "Bash", ...}, // Matches Bash
{"matcher": "Bash.*", ...}, // Also matches Bash!
{"matcher": ".*", ...} // Also matches everything!
]
}
}
Remove overlaps or make them mutually exclusive.
Environment Variables Not Working
Symptom
$CLAUDE_PROJECT_DIR or other variables are empty.
Solutions
Check variable spelling:
$CLAUDE_PROJECT_DIR(correct)$CLAUDE_PROJECT_ROOT(wrong)
Use double quotes:
{
"command": "$CLAUDE_PROJECT_DIR/hooks/script.sh"
}
In shell scripts, use from input:
#!/bin/bash
input=$(cat)
cwd=$(echo "$input" | jq -r '.cwd')
cd "$cwd" || exit 1
Debugging Workflow
Step 1: Enable debug mode
claude --debug
Step 2: Look for hook execution logs
[DEBUG] Executing hooks for PreToolUse:Bash
[DEBUG] Found 1 hook matchers
[DEBUG] Executing hook command: /path/to/script.sh
[DEBUG] Hook command completed with status 0
Step 3: Test hook in isolation
echo '{"test":"data"}' | /path/to/hook.sh
Step 4: Check script with set -x
#!/bin/bash
set -x # Print each command before executing
# ... your script
Step 5: Add logging
#!/bin/bash
echo "Hook started" >> /tmp/hook-debug.log
input=$(cat)
echo "Input: $input" >> /tmp/hook-debug.log
# ... your logic
echo "Decision: $decision" >> /tmp/hook-debug.log
Step 6: Verify JSON output
echo '{"decision":"approve","reason":"test"}' | jq .
If jq fails, JSON is invalid.