Your Claude Code hooks might not be doing what you think they're doing.
I've been running Claude Code autonomously for 108+ hours. In that time, I discovered that most hook problems don't throw errors — they just silently fail, leaving you thinking you're protected when you're not.
Here are the 5 mistakes I hit, how long each one cost me, and the fix.
1. Exit Code 1 Instead of Exit Code 2
The mistake:
#!/bin/bash
# "Security" hook that doesn't actually block anything
if echo "$TOOL_INPUT" | grep -q "rm -rf"; then
echo "Blocked dangerous command!"
exit 1 # ← THIS IS WRONG
fi
What actually happens: Exit code 1 means "error" — Claude Code logs it and continues anyway. Your "security gate" is a suggestion, not a gate.
The fix: Exit code 2 blocks the action. Exit 0 = pass. Exit 2 = block.
if echo "$TOOL_INPUT" | grep -q "rm -rf"; then
echo "Blocked dangerous command!"
exit 2 # ← Blocks the tool use
fi
Time lost: ~3 hours of autonomous operation before I noticed pushes weren't being blocked.
2. Using $HOME in Hook Paths
The mistake:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "$HOME/.claude/hooks/branch-guard.sh"
}]
}]
}
}
What actually happens: $HOME isn't expanded in the JSON config. The hook silently doesn't load.
The fix: Use the full absolute path:
"command": "/home/youruser/.claude/hooks/branch-guard.sh"
Or use ~ which Claude Code does expand:
"command": "~/.claude/hooks/branch-guard.sh"
How to verify: Run /hooks inside Claude Code — it shows which hooks are actually loaded.
3. Not Handling Missing Dependencies
The mistake:
#!/bin/bash
# Hook that requires jq, but what if jq isn't installed?
TOOL_NAME=$(echo "$TOOL_INPUT" | jq -r '.tool_name')
What actually happens: If jq isn't installed, the script errors out silently. Depending on your shell config, it might exit 0 (pass everything) or exit 1 (warn but don't block).
The fix: Check dependencies at the top:
#!/bin/bash
if ! command -v jq &>/dev/null; then
echo "WARN: jq not installed, hook disabled"
exit 0 # Fail open, or exit 2 to fail closed
fi
Decide your failure mode: Fail open (exit 0, let everything through) or fail closed (exit 2, block everything). For security hooks, fail closed. For monitoring hooks, fail open.
4. Hooks That Take Too Long
The mistake:
#!/bin/bash
# Syntax check that runs on EVERY tool use
python3 -m py_compile "$FILE" 2>&1
eslint "$FILE" 2>&1
What actually happens: Hooks run synchronously. A 3-second hook on every Edit/Write means 3 extra seconds per file change. During a refactoring session with 50 edits, that's 2.5 minutes of pure overhead.
The fix:
- Use matchers to narrow when hooks fire:
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "..." }]
}
Keep hooks under 500ms. Move expensive checks to PostToolUse (non-blocking) instead of PreToolUse (blocking).
Check only the changed file, not the whole project:
#!/bin/bash
FILE=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
[ -z "$FILE" ] && exit 0
# Only check THIS file, not everything
python3 -m py_compile "$FILE" 2>&1
5. No Context Window Monitoring
The mistake: Not having a hook for context window exhaustion.
What actually happens: Claude Code works fine for 45 minutes, then context hits 95%. Responses get shorter, hallucinations increase, and eventually the session compacts — losing your working state. No warning.
The fix: A PostToolUse hook that monitors context usage:
#!/bin/bash
# Simplified context monitor
# Full version: https://github.com/yurukusa/claude-code-hooks
# Count tool calls as a proxy for context usage
CALL_FILE="/tmp/cc-tool-count-$$"
COUNT=$(cat "$CALL_FILE" 2>/dev/null || echo 0)
COUNT=$((COUNT + 1))
echo "$COUNT" > "$CALL_FILE"
if [ "$COUNT" -gt 200 ]; then
echo "⚠ CONTEXT WARNING: $COUNT tool calls. Consider /compact soon."
fi
if [ "$COUNT" -gt 300 ]; then
echo "🚨 CONTEXT CRITICAL: $COUNT tool calls. /compact NOW or risk losing state."
fi
This is a simplified version. In production, you want graduated thresholds (CAUTION → WARNING → CRITICAL → EMERGENCY) and auto-save of your working state before compact.
Quick Checklist
Before you trust your hooks:
- [ ] Security hooks use
exit 2, notexit 1 - [ ] Paths in settings.json are absolute (no
$HOME) - [ ] Dependencies are checked (
command -v jq) - [ ] Heavy hooks use matchers to limit execution
- [ ] Context window is monitored
Run /hooks in Claude Code to verify your hooks are loaded. If they're not listed, they're not running.
The Bigger Picture
Hooks are Claude Code's most underused feature. Most people either don't use them at all, or set them up wrong and think they're protected.
The difference between "running Claude Code" and "running Claude Code safely" is usually 3-4 well-configured hooks. Not a dozen — just the right ones.
If you want to check your setup quickly: npx cc-health-check scores your configuration against 20 production checks. It takes 30 seconds and tells you exactly what's missing.
Running Claude Code autonomously? Claude Code Ops Kit ($19) — 10 hooks + 6 templates + 3 tools. Production-ready in 15 minutes.
Top comments (0)