Skip to content

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.md files
  • 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:

~/.claude/settings.json
{
"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:

Terminal window
chmod +x ~/.claude/hooks/*.sh

Example 1: Promise Checker Hook

I created a Stop hook that blocks the session from ending if Claude made promises without writing files.

~/.claude/hooks/promise-checker.sh
#!/bin/bash
# Get Claude's last response
LAST_RESPONSE="$1"
# Check for promise words
if 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
fi
fi
exit 0

When 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 tools
Please 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.

~/.claude/hooks/console-log-audit.sh
#!/bin/bash
# Get list of modified files from environment
MODIFIED_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."
fi
done
exit 0

Before the session ends, I see:

⚠️ WARNING: console.log found in src/utils/validation.ts
Please 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.

~/.claude/hooks/typescript-check.sh
#!/bin/bash
# Run TypeScript compiler
npx tsc --noEmit
if [ $? -ne 0 ]; then
echo "❌ TypeScript errors detected. Please fix before continuing."
exit 1 # Block further tool use until fixed
fi
exit 0

The 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.

~/.claude/hooks/startup.sh
#!/bin/bash
# Only run on first tool call
if [[ -f "/tmp/claude-startup-complete" ]]; then
exit 0
fi
echo "📋 Loading project context..."
echo "📖 Reading journal entries..."
echo "🔍 Running signal detection..."
# Present self-check questions
echo ""
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-complete
exit 0

At 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:

  1. CLAUDE.md: Rules in context → Claude CAN ignore
  2. Skills: Specialized workflows → Claude CAN ignore
  3. 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

Terminal window
# This won't work
chmod 644 ~/.claude/hooks/promise-checker.sh
# Must be executable
chmod +x ~/.claude/hooks/promise-checker.sh

Warning instead of blocking

Terminal window
# This is too weak - Claude can ignore it
if [[ -z "$WRITE_TOOLS_CALLED" ]]; then
echo "⚠️ You forgot to write"
exit 0 # Lets session continue
fi
# This actually enforces the rule
if [[ -z "$WRITE_TOOLS_CALLED" ]]; then
echo "❌ BLOCKED"
exit 1 # Non-zero blocks the operation
fi

Complex 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