
What Are Claude Code Hooks and How Do You Use Them?
Carlos GarciaPublished What Are Claude Code Hooks and How Do You Use Them?
There is a difference between telling an agent what to do and making it so. Most of the ways you configure Claude Code fall into the first category.
A line in a project instruction file is a request. It is a good request, usually followed, and occasionally overlooked when the context gets long or the task gets complicated.
Hooks are the other category. A hook is code that runs on an event, and when it says no, the answer is no.
That makes hooks the right tool for the handful of rules you cannot afford to have politely ignored, and the wrong tool for everything else.
This guide covers what a hook is, where they are configured, how the blocking contract works, and when a hook is the wrong answer.
What Are Claude Code Hooks? The Direct Answer
A hook is a command, HTTP call, or prompt that Claude Code runs automatically at a defined point in a session. You configure them in a settings JSON file under a hooks key.
Each hook is attached to an event. The most used are PreToolUse, which fires before a tool call executes, and PostToolUse, which fires after one succeeds.
The hook receives the event as JSON on standard input, does whatever you wrote it to do, and signals the outcome with its exit code.
Exit 0 means proceed. Exit 2 means block, with your message on standard error as the reason. Any other code is a non-blocking error, and the action goes ahead anyway.
Enforcing your own standards is a good habit. Get a free SEO audit and see which standards your site is currently failing.
Free audit
Where do you stand in AI search?
A manual SEO + AI search audit of your site: how ChatGPT, Gemini, Perplexity and Claude read and cite it today, and what to fix first. Report within 48 hours, no credit card.
Get my free auditOr have it all done: AI SEO services, $999 once
What a Hook Actually Is
A hook is not a plugin, a skill, or an instruction. It is deterministic code sitting on an event in the agent's lifecycle, and it runs whether or not the model thinks it should.
Where hooks live
There are several places to put them, and the one you choose decides the scope.
~/.claude/settings.jsonapplies to all your projects, on your machine only..claude/settings.jsonapplies to one project and can be committed to the repository, so the whole team gets it..claude/settings.local.jsonapplies to one project and stays out of version control.- Managed policy settings are controlled by an administrator and apply across an organization.
- A plugin can ship hooks in its own
hooks/hooks.json, active while the plugin is enabled.
The project-level shareable file is the interesting one. A hook committed there is a convention that travels with the code rather than living in somebody's personal setup.
The three levels of nesting
The configuration shape catches people out, because there are three levels before you get to the actual command. Event, then matcher group, then handler.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check.sh",
"timeout": 30
}
]
}
]
}
}The matcher decides which tool calls the hook applies to. A plain name like Bash is an exact match, and Edit|Write matches either.
Leave the matcher empty, set it to "*", or omit it entirely, and the hook fires on everything. Events that have no concept of a matcher ignore the field.
The type field accepts more than shell commands. As well as command, there is http, mcp_tool, prompt, and an experimental agent type.
A timeout is worth setting. Command hooks default to 600 seconds, which is a long time to wait for a script that was only ever meant to take one.
What a hook receives
Command hooks get the event as JSON on standard input. Several fields are present on every event, whichever one fired.
session_idandprompt_ididentify the session and the current turn.cwdis the working directory, andtranscript_pathpoints at the conversation log.permission_modetells you whether the session is in default, plan, or auto mode.hook_event_nameis the event that fired, which lets one script serve several hooks.
Event-specific fields are added on top. A PreToolUse payload also carries tool_name, tool_input, and tool_use_id, which is everything you need to make a decision about a tool call.
How to Set Up Your First Hook, Step by Step
The shortest useful hook is one that logs every shell command Claude runs. It takes about five minutes.
- Create the hooks directory. Make a
.claude/hooks/folder in your project if one does not exist yet. - Write the script. Have it read JSON from standard input, pull out the field you care about, and append a line to a log file. Any language works.
- Make it executable. A hook that is not executable fails as a non-blocking error, which means it fails quietly and the action proceeds.
- Register it in settings. Add a
PreToolUseentry with a matcher ofBashand your script's path as the command. - Use the project directory variable. Reference the script as
${CLAUDE_PROJECT_DIR}/.claude/hooks/your-script.shrather than a relative path, which depends on the working directory. - Restart the session and test it. Ask Claude to run a trivial shell command, then check that your log file grew.
- Only then add blocking. Get the logging version working before you let a script return exit 2, or you will spend your afternoon debugging a hook that blocks everything.
That last step is the one worth taking seriously. A broken blocking hook on a wide matcher can make a session feel completely stuck for no visible reason.
Shipping fast is only useful if the pages get found. Get a free SEO audit and get a prioritised list of fixes.
The Exit Code Contract
The exit code is how a hook communicates, and the contract has one genuine trap in it.
Exit 0 means success. The action proceeds. If your script printed a single JSON object to standard output, Claude Code applies the fields in it.
Exit 2 means a blocking error. On events that support blocking, the action is stopped and whatever your script wrote to standard error becomes the stated reason.
Any other exit code is a non-blocking error. A notice appears, and the action goes ahead regardless.
The trap is exit 1. Scripts fail with exit 1 by convention almost everywhere else, so the instinct is to assume a failing hook blocks. It does not.
If a hook is meant to enforce a policy, it has to exit 2 specifically. A hook that exits 1 announces a problem and then lets the thing happen anyway.
For finer control than the exit code allows, a hook can return JSON on standard output. A PreToolUse hook can set permissionDecision to allow, deny, ask, or defer, with a reason attached.
Other useful fields include additionalContext, which adds text to Claude's context, and updatedInput, which rewrites the tool's arguments before it runs.
When to Reach for a Hook
Hooks are narrow by design. The cases where they clearly beat the alternatives have a shape in common: something must happen every time, not usually.
Formatting and linting. A PostToolUse hook on Edit|Write that runs your formatter means the code is formatted whether or not anyone remembered to ask.
Guardrails on destructive commands. A PreToolUse hook on Bash can refuse a pattern you never want run against production, and refusal means refusal.
Audit logging. Appending every tool call to a file gives you a record that does not depend on the agent choosing to mention what it did.
Notifications. A Notification or Stop hook can ping you when a long task finishes, which is more reliable than watching a terminal.
Injecting context. A SessionStart hook can pull in the current branch, open tickets, or deployment state so you are not pasting it in by hand.
Limitations and Risks Worth Knowing
Hooks run with your user's permissions and inherit your environment. That is the point, and it is also the main thing to be careful about.
A hook from an unfamiliar source is code you are agreeing to run on your own machine. The same scrutiny you would give any dependency applies here.
Hooks are not a gate by default. A PreToolUse hook that times out does not block the call unless you set onFailure to block. Silence is not a denial.
The if filter is best effort. For a hard allow or deny, the documentation points you at the permission system rather than at a hook.
Added context is still text. Material injected through additionalContext should read as factual statements. Phrasing it as out-of-band instructions can trip Claude's own prompt-injection defences.
Administrators can override you. An organization can set allowManagedHooksOnly, which disables user, project, and plugin hooks, and managed hooks cannot be switched off from the user side.
No terminal. Command hooks have no controlling terminal and cannot write to /dev/tty, so a hook cannot prompt you interactively.
There is also a maintenance cost. A hook that blocks on a pattern nobody remembers adding is a confusing thing to inherit six months later.
Guardrails catch the problems you thought of. Get a free SEO audit and find the ones you did not.
Hooks vs the Alternatives
Claude Code has several configuration surfaces and they are easy to confuse. The honest comparison is about enforcement, not features.
An instruction file is the lightest option and the right default. It shapes behaviour across everything, costs nothing to change, and can be overlooked.
A skill packages knowledge and procedure the agent can draw on. It is documentation that gets loaded when relevant, not a rule that fires on an event.
An MCP server adds capability. It gives the agent new tools rather than constraining the ones it already has.
A hook is enforcement. It is the only one of the four that can stop something from happening, and it is also the only one you have to maintain as code.
The verdict is straightforward. Start with an instruction file. Move a rule to a hook only when you discover that "usually followed" is not good enough for that particular rule.
Frequently Asked Questions
Do I need to restart after editing hooks? Yes, start a new session. Configuration is read at startup rather than watched for changes.
Can a hook see what Claude said? Stop and SubagentStop hooks should read last_assistant_message rather than the transcript file, which can lag the current turn.
Can one script handle several events? Yes. Switch on the hook_event_name field in the payload and register the same path under each event.
Final Thoughts
Hooks are a small feature with a sharp edge. They are the one configuration surface in Claude Code that enforces rather than suggests, which makes them valuable and worth using sparingly.
Get the mechanics right and most of the difficulty disappears: three levels of nesting, JSON in on standard input, exit 2 to block, and nothing else to block with.
If you find yourself wanting to share a set of hooks across several repositories, that is the point at which bundling them makes sense. Our guide to how Claude Code plugins work covers packaging hooks alongside skills and agents in one install.
And start with a hook that only logs. The blocking version is five minutes away once you can see that the thing is firing at all.
Guardrails on your code are half the job. Get a free SEO audit and find out what is quietly broken on your site.
Want this done for you?
SEO Stuff gets businesses cited by ChatGPT, Gemini, Perplexity and Claude, and ranking in Google. Start with the free audit, a call, or the package.
See where you stand
A free, manual SEO + AI search audit of your site, with a report within 48 hours. No credit card, no sales call.
Get my free auditTalk it through
A free 15-minute call with the founder about whether AI search is a meaningful opportunity for your business.
Free strategy callHave it done
The Done-For-You Package: audit, 10 pages of content, 3 DR50+ placements, dashboard. $999 once, delivered in 21 business days.
See the package

