What Are Hooks in Claude Code and How to Use Them?
The Problem
When I use Claude Code for long sessions, I notice it gradually drifts from my instructions. It follows rules perfectly for 20 minutes, then starts finding “reasonable” reasons to skip steps. The worst pattern is when Claude says “I’ll write that down” or “noted” but then never actually calls the Write or Edit tool.
I tried everything:
- Putting rules in
CLAUDE.mdfiles - Creating specialized skills
- Repeating instructions in every prompt
But Claude still forgets to write files. The rules are there, Claude reads them, understands them, but can still choose to ignore them.
Why Rules Don’t Work
I think the core issue is that both CLAUDE.md rules and skills are suggestions, not requirements. They exist in Claude’s context, but Claude decides whether to follow them. During long sessions, Claude finds logical reasons to bypass rules:
User: "Remember to write that function to the utils file."Claude: "Noted! I'll add it to utils/validation.ts"
[Five minutes later, Claude moves to next task without writing]The promise happened, but no Write tool was called. This is where most people give up and manually add the forgotten code themselves.
The Solution: Hooks
Hooks are external shell scripts that execute automatically on specific events. The key difference: hooks run outside Claude’s control loop, so Claude cannot ignore or bypass them.
There are three hook types:
- PreToolUse: Executes before any tool call
- PostToolUse: Executes after tool completion
- Stop: Executes when session ends
Here’s my ~/.claude/settings.json configuration:
{ "hooks": { "PreToolUse": [ { "pattern": "Bash", "command": "~/.claude/hooks/tmux-reminder.sh" } ], "PostToolUse": [ { "pattern": "Edit.*\\.ts$", "command": "~/.claude/hooks/prettier.sh" } ], "Stop": [ { "command": "~/.claude/hooks/promise-checker.sh" }, { "command": "~/.claude/hooks/console-log-audit.sh" } ] }}The hooks live in ~/.claude/hooks/ and must be executable:
chmod +x ~/.claude/hooks/*.shExample 1: Promise Checker Hook
I created a Stop hook that blocks the session from ending if Claude made promises without writing files.
#!/bin/bash
# Get Claude's last responseLAST_RESPONSE="$1"
# Check for promise wordsif echo "$LAST_RESPONSE" | grep -qiE "I'll (write|remember|save|note|create|add)|noted|saved"; then # Check if Write or Edit tool was called # (This info is passed as environment variable by Claude Code) if [[ -z "$WRITE_TOOLS_CALLED" ]]; then echo "❌ BLOCKED: You promised to write something but didn't call Write/Edit tools" echo "Please complete the write action before ending the session." exit 1 # Non-zero exit blocks session end fifi
exit 0When I try to close a session where Claude said “I’ll write that” but didn’t, I get:
❌ BLOCKED: You promised to write something but didn't call Write/Edit toolsPlease complete the write action before ending the session.
[Session blocked - cannot exit]This single hook eliminated 80% of my “forgot to write” failures.
Example 2: Console Log Audit
Another Stop hook warns about console.log statements in any modified files.
#!/bin/bash
# Get list of modified files from environmentMODIFIED_FILES="$MODIFIED_FILES"
for file in $MODIFIED_FILES; do if grep -q "console.log" "$file" 2>/dev/null; then echo "⚠️ WARNING: console.log found in $file" echo "Please remove or replace with proper logging before committing." fidone
exit 0Before the session ends, I see:
⚠️ WARNING: console.log found in src/utils/validation.tsPlease remove or replace with proper logging before committing.Example 3: TypeScript Auto-Check
I use a PostToolUse hook that runs TypeScript compiler after any .ts file edit.
#!/bin/bash
# Run TypeScript compilernpx tsc --noEmit
if [ $? -ne 0 ]; then echo "❌ TypeScript errors detected. Please fix before continuing." exit 1 # Block further tool use until fixedfi
exit 0The configuration in settings.json:
{ "hooks": { "PostToolUse": [ { "pattern": "Edit.*\\.(ts|tsx)$", "command": "~/.claude/hooks/typescript-check.sh" } ] }}Now when I edit a TypeScript file:
[Edit tool completes on src/utils/validation.ts][Hook triggers: typescript-check.sh]
❌ TypeScript errors detected. Please fix before continuing.[Further tool use blocked until errors are fixed]Example 4: Startup Hook
I created a PreToolUse hook that loads context on first interaction.
#!/bin/bash
# Only run on first tool callif [[ -f "/tmp/claude-startup-complete" ]]; then exit 0fi
echo "📋 Loading project context..."echo "📖 Reading journal entries..."echo "🔍 Running signal detection..."
# Present self-check questionsecho ""echo "🤔 Session Self-Check:"echo " • What is the primary goal for this session?"echo " • Are there any blocked tasks from previous session?"echo " • What's the success criteria?"
touch /tmp/claude-startup-completeexit 0At the start of each session:
📋 Loading project context...📖 Reading journal entries...🔍 Running signal detection...
🤔 Session Self-Check: • What is the primary goal for this session? • Are there any blocked tasks from previous session? • What's the success criteria?How It Works
The hook system passes data to scripts through environment variables and arguments:
- PreToolUse/PostToolUse: Receives tool name, parameters, and result
- Stop: Receives the full conversation history and modified files list
A hook can:
- Block operations by returning non-zero exit code
- Modify parameters by printing updated JSON to stdout
- Warn users by printing messages
- Perform side effects like running tests or formatters
The critical insight from the Reddit discussion is the enforcement hierarchy:
- CLAUDE.md: Rules in context → Claude CAN ignore
- Skills: Specialized workflows → Claude CAN ignore
- Hooks: External scripts → Claude CANNOT ignore
Why This Matters
Hooks provide actual enforcement instead of suggestions. When I use the promise checker hook, Claude cannot end the session without fulfilling its promises. The hook executes outside Claude’s decision loop, so there’s no way to bypass it.
This pattern solved my biggest frustration with Claude Code: forgotten actions and rule drift over long sessions. The hooks don’t depend on Claude’s attention or memory — they run independently and enforce rules mechanically.
Common Mistakes
I made several mistakes when I started with hooks:
Not making scripts executable
# This won't workchmod 644 ~/.claude/hooks/promise-checker.sh
# Must be executablechmod +x ~/.claude/hooks/promise-checker.shWarning instead of blocking
# This is too weak - Claude can ignore itif [[ -z "$WRITE_TOOLS_CALLED" ]]; then echo "⚠️ You forgot to write" exit 0 # Lets session continuefi
# This actually enforces the ruleif [[ -z "$WRITE_TOOLS_CALLED" ]]; then echo "❌ BLOCKED" exit 1 # Non-zero blocks the operationfiComplex hook logic Keep hooks simple and fast. A hook that takes 10 seconds to run will slow down every tool call. I learned to do quick checks in hooks and defer heavy work to background processes.
Summary
In this post, I showed how hooks in Claude Code provide guaranteed rule enforcement by executing outside Claude’s decision loop. The key point is that unlike CLAUDE.md rules or skills, hooks cannot be ignored or bypassed. I demonstrated four working examples: a promise checker that eliminated 80% of forgotten writes, a console.log auditor, TypeScript auto-checking, and a startup workflow hook.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments