@sairahul1: https://x.com/sairahul1/status/2069710540654645550
Summary
A comprehensive guide explaining Claude Code Hooks, which are programmable checkpoints that run before actions to enforce rules and block or allow tool calls, offering more reliable control than CLAUDE.md guidance.
View Cached Full Text
Cached at: 06/24/26, 02:26 PM
Claude Code Hooks: The Most Powerful Feature Nobody Uses
CLAUDE.md tells Claude how to behave.
Hooks make Claude actually obey.
That is the difference most people miss.
You can write “do not modify prod.env” in CLAUDE.md.
But whether Claude follows it depends entirely on Claude’s judgment in that moment.
Hooks bypass that judgment.
They are programmable checkpoints that run inside Claude Code’s execution flow — before the action happens, not after.
This is the complete guide.
The problem Hooks solve
Claude Code is powerful.
It reads files. Writes code. Executes commands. Calls APIs.
The more you give it, the more useful it becomes.
But also the more dangerous.
What stops it from modifying a production config at 2am?
What enforces your linting rules every single time?
What logs every sensitive file read without relying on Claude remembering to do it?
CLAUDE.md is guidance.
Hooks are guarantees.
The difference: Hooks run real code. Their logic does not depend on whether the model understands or remembers the rule. It depends on the code you wrote in advance.
What a Hook actually is
A Hook is not a prompt.
It is not another way to inject context.
It is a programmable control mechanism that runs inside Claude Code’s execution flow.
When Claude Code is about to call a tool, write a file, or execute a command — a Hook steps in before the action happens and decides:
→ Allow it → Block it → Ask a human to confirm it
The decision is made by code you wrote in advance.
Not by the model.
Here is the minimal Hook configuration in settings.json:
json
json{ “hooks”: { “PreToolUse”: [ { “matcher”: “Write”, “hooks”: [ { “type”: “command”, “command”: “echo ‘about to write a file’” } ] } ] } }
The word “hooks” appears three times. Each means something different.
Let me unpack the three layers once.
Layer 1 — The event registry.
The outer hooks object. Each key is an event name — PreToolUse, PostToolUse, Stop. Where do you want to intervene? Register that event here.
Layer 2 — The matching rule.
Under each event: an array of matcher objects. matcher: “Write” means this group only triggers when Claude tries to call the Write tool. No matcher = matches everything.
Layer 3 — The actual Hook.
Inside each matcher: the Hooks array. This is the real logic. type: command runs a shell script. type: http calls a URL. type: mcp_tool calls an MCP tool.
hooks ← Entry point for the whole system └── PreToolUse ← Event: fires before any tool call └── matcher ← Filter: only match “Write” tool └── hook ← Action: run this shell command
The Hook event system
Claude Code has 28 Hook events.
They cover every key point in Claude’s execution:
→ Sessions starting and stopping → Tool calls before and after → Permission requests → File changes → Subagent tasks → Notifications
One thing most people get wrong: events are siblings, not parents and children.
PreToolUse and PermissionRequest often fire back to back. This makes it look like one causes the other.
They don’t. They are completely independent intervention points. Each has its own matching rules and executes without interfering with the other.
The events split into two types:
Main-flow events — run through the core execution path. These can block Claude. PreToolUse, PermissionRequest, PostToolUse, Stop.
Side-path events — observation and notification channels. They fire at the right moment but don’t change the main flow. Notification, ConfigChange.
The critical difference between them:
→ Blocking: Hook result determines what Claude does next. Main flow pauses until the Hook returns. → Non-blocking: Hook runs, but Claude doesn’t wait for it. Useful for logging, pushing notifications, syncing state.
How to actually block Claude
This is the part the documentation buries.
Two ways to block a blocking event. They mean completely different things.
Exit 2 → System error
Claude thinks something broke. A tool is unavailable, resources are missing, the environment is broken. It may try to understand the error. It may try a workaround.
bash#!/bin/bash
Block with system error signal
if [[ “$TOOL_INPUT” == “prod.env” ]]; then echo “Environment error: target file locked” >&2 exit 2 fi Exit 0 + JSON → Policy rejection
Claude understands this as an explicit business rule saying the operation is not allowed. It doesn’t try to bypass it. It accepts the decision and adjusts behavior.
bash#!/bin/bash
Block with policy decision (much better for rules)
TOOL_INPUT=(cat) FILE=(echo “$TOOL_INPUT” | jq -r ‘.path // “”’)
if [[ “$FILE” == “prod.env” ]]; then echo ‘{ “decision”: “deny”, “reason”: “Writing to prod.env is not allowed. Use staging.env instead.” }’ exit 0 fi
Allow everything else
echo ‘{“decision”: “allow”}’ exit 0
The JSON approach does more too:
→ Attach a readable rejection reason → Modify tool input parameters before Claude uses them via updatedInput → Ask for human confirmation instead of auto-denying via “decision”: “ask”
The mistake everyone makes:
exit 1 does nothing. In Unix conventions it means failure. In Claude Code’s Hook system, exit 1 is non-blocking. Claude ignores it and continues.
Only exit 2 or exit 0 + JSON actually affects the flow.
Where Hooks live
Hooks don’t only live in your personal settings.json.
They can be registered in 4 different places. Each has a different scope and lifecycle.
1. Settings-level Hooks — the resident Hooks.
Written in settings.json at user, project, or local level. Active from the start of Claude Code to the end. Never cleaned up between tasks.
~/.claude/settings.json ← user-level, your machine .claude/settings.json ← project-level, shared with team .claude/settings.local.json ← local overrides, not committed
2. Plugin Hooks — loaded with the plugin.
A Plugin bundles its own CLAUDE.md, Skills, and Hooks. When Claude Code loads the Plugin, its Hooks merge with the main Hooks and run equally. No priority difference — they participate together.
One hard limit: Plugin Subagents cannot define Hooks. This is intentional. A Subagent is a restricted execution unit. Letting it register Hooks would give a lower-privileged role the ability to modify execution-flow control. That breaks the security model.
3. Skill Hooks — scoped to the Skill.
Registered when the Skill is invoked. Automatically cleaned up when the Skill finishes. They don’t pollute the global environment.
yaml— name: planning-with-files hooks: PreToolUse: - matcher: “Write|Edit|Bash|Read” hooks: - type: command command: “cat task_plan.md 2>/dev/null | head -30 || true” PostToolUse: - matcher: “Write|Edit” hooks: - type: command command: “echo ‘File updated. Update task_plan.md status.’” Stop: - hooks: - type: command command: “./scripts/final-check.sh”
Every time this Skill calls Write, Edit, or Bash, it first prints the first 30 lines of the task plan. After every file write, it reminds Claude to update plan status. When the Skill ends, it runs a final check.
4. Subagent Hooks — scoped to the Subagent.
Same as Skill Hooks. Temporary, auto-cleaned-up. One extra behavior: if you register a Stop Hook in a Subagent’s frontmatter, it automatically converts to SubagentStop at runtime. Because what ends is the Subagent, not the whole session.
How multiple Hooks merge
At any moment, Hooks from multiple layers may be active simultaneously.
When a Write operation fires PreToolUse, it might match Hooks from your user settings, your project config, and the currently active Skill — all at once.
Three rules govern what happens.
Rule 1: Parallel execution.
All matched Hooks run simultaneously. Not serially. Not by priority order.
Your logging Hook and your security check Hook start at the same time and complete independently. Claude waits for all of them before deciding.
Rule 2: Automatic deduplication.
If two layers register the exact same Hook — same event, same matcher, same command string — Claude Code keeps only one copy and runs it once.
This matters in practice: you don’t need to worry about the same script running twice if it appears in multiple config files. But the reverse: if you want the same script to run separately under two different conditions, make sure their command strings are different.
Rule 3: Strictest result wins.
When multiple Hooks return different decisions, Claude picks the strictest one.
deny > ask > allow
One deny is enough. It doesn’t matter which layer it came from.
User-level Hook: allow (ordinary writes are fine) Project-level Hook: ask (.env files need confirmation) Plugin Hook: deny (prod.env is forbidden) ───────────────────────────── Final decision: deny (one veto is enough)
Why this design makes sense: in a security system, allowing requires everyone to agree. Rejecting only needs one veto. This is how every serious security model works.
Two real Hooks worth studying
Theory is easy. Let’s look at two production Hooks and understand why they were designed the way they were.
Case 1: Superpowers plugin — inject context at session start
The superpowers plugin provides a complete Skill system for engineering discipline: requirement clarification, planning, test-driven development, code review.
But it only registers one Hook.
json{ “hooks”: { “SessionStart”: [ { “matcher”: “startup|clear|compact”, “hooks”: [ { “type”: “command”, “command”: “"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd" session-start” } ] } ] } }
One event. One Hook. What does it do?
It injects the Superpowers skill instructions into every session as additional context. Every time a session starts, Claude gets the right initial context automatically.
The insight: Hooks don’t always have to block or approve. Sometimes bringing the right information in at the right moment is the entire job.
Case 2: claude-code-warp plugin — bridge Claude’s lifecycle to a terminal
Warp is a terminal. When you use Claude Code inside Warp, Warp had no idea what Claude was doing — working, waiting, requesting permission, done.
The claude-code-warp plugin fixes this with 6 Hooks:
SessionStart → sends initialization info to Warp UserPromptSubmit → tells Warp: Claude started working PostToolUse → tells Warp: blocking state cleared Notification → triggers Warp notification when Claude is idle PermissionRequest → sends tool name + input preview to Warp Stop → reads session, extracts summary, sends completion notification
The Stop Hook is the most interesting one.
It doesn’t just say “done.” It reads the current session content, extracts the last user prompt and Claude’s response, truncates to notification-friendly length, and sends a structured summary to Warp’s notification center.
One Hook turns Claude Code’s internal session record into a completion notification the user can actually read.
The insight: Hooks can act as event bridges — synchronizing Claude Code’s execution state to external systems that have no native awareness of Claude.
A practical Hook you can use today
Protect your production environment files. One script. Copy-paste ready.
Create .claude/hooks/protect-prod.sh:
bash#!/bin/bash
Read tool input from stdin
TOOL_INPUT=$(cat)
Extract file path from the input JSON
FILE_PATH=(echo "TOOL_INPUT“ | jq -r ‘.path // .file_path // “”’)
Check if target is a production config file
PROTECTED_PATTERNS=(“prod.env” “.env.production” “prod-secrets” “production.yaml”)
for pattern in “{PROTECTED_PATTERNS[@]}"; do if [[ "FILE_PATH” == “$pattern” ]]; then echo ‘{ “decision”: “deny”, “reason”: “’”$FILE_PATH“’ is a protected production file. Use staging environment instead. If you genuinely need to edit production config, do it manually.“ }’ exit 0 fi done
Not a protected file — allow
echo ‘{“decision”: “allow”}’ exit 0
Register it in .claude/settings.json:
json{ “hooks”: { “PreToolUse”: [ { “matcher”: “Write|Edit”, “hooks”: [ { “type”: “command”, “command”: “bash .claude/hooks/protect-prod.sh” } ] } ] } }
Make it executable:
bashchmod +x .claude/hooks/protect-prod.sh
Now every Write and Edit that targets a production config is blocked — with a clear reason — before Claude touches the file. Not because Claude remembers the rule. Because the code enforces it.
The mental model that makes Hooks click
Think of Claude Code’s execution flow as a pipeline.
Without Hooks: Claude runs the whole pipeline. CLAUDE.md guides it. Skills organize it. But the execution itself is uninterrupted.
With Hooks: you insert checkpoints at any point in the pipeline. Each checkpoint runs real code. It can inspect what Claude is about to do, decide whether to allow it, and optionally modify the input before Claude uses it.
The system has three layers working together:
→ CLAUDE.md — tells Claude how to understand the project → Skills — organizes Claude into reliable workflows → Hooks — guards the boundary at key execution points
None of these replaces the others.
CLAUDE.md without Hooks: good guidance, inconsistent enforcement. Hooks without CLAUDE.md: strict enforcement of rules the model doesn’t understand. Both together: reliable behavior at every level.
One warning before you go build Hooks.
Hooks run real code. Effects are immediate and sometimes irreversible.
A script with the wrong exit code can unexpectedly interrupt the flow.
A Stop Hook with poorly handled exit logic can trap the session in a loop.
Test every Hook the way you would test production code.
Handle edge cases. Handle error paths. Log failures explicitly.
The power of Hooks is that they can’t be ignored.
That same property makes bugs in Hooks expensive.
5 things to remember
→ 1. Hooks are not prompts. They are code. They run regardless of model state.
→ 2. exit 1 does nothing. Use exit 2 for system errors. Use exit 0 + JSON for policy decisions.
→ 3. Events are siblings. PreToolUse and PermissionRequest fire independently. One does not cause the other.
→ 4. Strictest result wins. One deny from any layer blocks the operation. Allow requires everyone to agree.
→ 5. Skill and Subagent Hooks are temporary. They register on invocation and clean up on completion. They don’t pollute global state.
If this was useful:
→ Repost to share it with every developer building with Claude Code → Follow @sairahul1 for more deep dives like this → Bookmark this
I write about AI, building products, and systems that work without you.
Similar Articles
luongnv89/claude-howto
A comprehensive, structured guide for mastering Claude Code features, including slash commands, hooks, skills, MCP servers, and subagents, with visual tutorials, copy-paste templates, and a progressive learning path from beginner to power user.
@humzaakhalid: Most developers are using Claude Code wrong (100%) Claude Code fixes it in 90 seconds flat. Here is exactly how: 1. Get…
A detailed guide on using Claude Code effectively, including setup, CLAUDE.md configuration, four-layer architecture, hooks, and daily workflow patterns.
@sairahul1: I genuinely don't understand why everyone isn't using this yet. There is one Claude Code feature that: → runs your test…
Sai Rahul highlights Claude Code's Hooks feature that automates test running after edits, blocks destructive bash commands, logs spending, sends Slack alerts, and rewrites bad output automatically.
@tom_doerr: Practical guide to Claude Code skills, hooks, and agents https://github.com/wesammustafa/Claude-Code-Everything-You-Nee…
A comprehensive practical guide to Claude Code covering setup, skills, hooks, MCP, agent teams, and prompt engineering for developers.
@akshay_pachaar: Claude Code isn't a coding tool. (It's a programmable dev environment) Engineers open it, type a prompt, and let it wri…
Claude Code is positioned as a programmable dev environment rather than a simple coding tool, with 12 features including persistent memory (CLAUDE.md), behavioral rules, reusable skills, event hooks, slash commands, plugins, MCP connections, plan mode, permissions, subagents, voice mode, and rewind checkpoints.