Three months. That's how long I let Claude Code hooks sit in the docs, unread, while I complained that the AI kept producing code that skipped my formatting conventions. I'd set up Claude Code in January, started shipping features with it almost immediately, and told myself I'd "get to the hooks thing later". Classic.
Turned out the hooks weren't a nice-to-have. They were the missing piece that made Claude Code actually fit inside a real agency workflow rather than being a very expensive autocomplete that occasionally ignored ESLint.
What Claude Code Hooks Actually Are (No Vague Abstractions)
Hooks are shell commands that Claude Code fires at specific points during its own operation. Think of them as lifecycle events, similar to Git hooks if you've ever written a pre-commit script, but wired into the AI's tool-use loop instead of Git's commit pipeline.
There are four event types right now:
PreToolUse, runs before Claude calls any tool (file edits, bash commands, etc.)PostToolUse, runs after a tool call completesNotification, triggers when Claude sends you a notificationStop, runs when Claude finishes its full response turn
You configure them in a settings.json file inside your project's .claude/ folder, or globally in ~/.claude/settings.json if you want them everywhere. Each hook gets a matcher (which tool or event triggers it) and a hooks array of shell commands to run.
The output from your hook gets fed back into Claude's context. That last bit is what makes this genuinely interesting rather than just a fancy cron job.
Why This Is Different From Just Running Scripts Yourself
You could manually run Prettier after every Claude edit. I did, for about two weeks, until I forgot during a deadline push and pushed a PR with 47 formatting violations. The hooks run automatically, inside the session, and Claude can read their output. So if your linter throws a warning, Claude sees it and can act on it in the same session. That feedback loop is the whole point.
The Setup That Actually Worked for Seahawk Projects
I'm going to be specific here because the generic "add hooks to your config" advice you'll find in most write-ups is useless without context.
At Seahawk, a big chunk of our work is WordPress builds and WooCommerce customisations. We also do React frontends for headless setups, and we've been using Next.js heavily since 2022. The hook configuration I landed on addresses problems specific to those stacks.
Here's the settings.json structure I use for a Next.js project:
`` { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit|MultiEdit", "hooks": [ { "type": "command", "command": "npx prettier --write $CLAUDE_FILE_PATHS && npx eslint --fix $CLAUDE_FILE_PATHS" } ] } ], "Stop": [ { "matcher": ".*", "hooks": [ { "type": "command", "command": "npx tsc --noEmit 2>&1 | head -20" } ] } ] } } ``
The PostToolUse hook fires Prettier and ESLint immediately after Claude touches a file. The Stop hook runs a TypeScript type-check at the end of every turn and surfaces the first 20 lines of any errors. Claude reads that output, and if there are type errors, it fixes them before I even see the response.
That TypeScript check alone saved me probably four hours last month on a fintech dashboard project where the client had strict noImplicitAny set. Claude kept generating any types in utility functions. After I added the Stop hook, it started self-correcting inside the same turn.
The Hooks I Use on WordPress / PHP Projects
WordPress is a different beast. No TypeScript, obviously, but PHP_CodeSniffer with the WordPress Coding Standards ruleset is what keeps things sane. Back in 2022 I had a junior dev on a WooCommerce project who went two weeks without running PHPCS. The code review was... not pleasant.
For PHP-heavy projects my PostToolUse hook runs:
`` vendor/bin/phpcs --standard=WordPress $CLAUDE_FILE_PATHS 2>&1 | tail -30 ``
And I pair that with a PreToolUse hook on bash commands:
`` { "matcher": "Bash", "hooks": [ { "type": "command", "command": "echo 'Bash tool triggered' >> ~/.claude/audit.log && date >> ~/.claude/audit.log" } ] } ``
That second one is pure paranoia. It writes every bash command Claude runs to an audit log. When you're running Claude Code on a live staging environment (yes, I've done it, yes it's a bit risky), knowing exactly what shell commands fired is genuinely reassuring.
Blocking Behaviour With Hook Exit Codes
This part of the docs took me a while to find. If your hook exits with code 2 , Claude Code treats that as a block and won't proceed with the tool call. Exit code 0 is success, anything non-zero (but not 2) just feeds the stderr back as context.
So you can write a PreToolUse hook that actually prevents Claude from doing something. I use this on projects that have a migrations/ directory I don't want Claude touching autonomously:
`` #!/bin/bash if echo "$CLAUDE_FILE_PATHS" | grep -q "migrations/"; then echo "Migrations folder is protected. Do not edit migration files autonomously." exit 2 fi exit 0 ``
That script lives at .claude/hooks/guard-migrations.sh . When Claude tries to write to anything under migrations/, it gets blocked and sees the message. It then asks me to confirm before proceeding. Simple, effective.
This is the kind of control that makes the difference between "I sort of trust this AI with my codebase" and "I actually trust it with my codebase".
Practical Hook Patterns Worth Stealing
These aren't theoretical. Each one came from a specific pain point.
- Auto-run tests after file edits. I run
npx jest --testPathPattern=$CLAUDE_FILE_PATHS --passWithNoTestsin aPostToolUsehook. It only runs tests related to the file Claude just edited, not the whole suite. Fast enough to be non-annoying. - Commit-ready formatting snapshot. A
Stophook that runsgit diff --statand feeds the summary back to Claude. It sees exactly what changed across the session, which helps it write a sensible commit message if I ask for one. - Environment variable safety check. A
PreToolUsehook onWritethat greps for hardcoded secrets patterns (things that look like API keys or passwords). If it finds something suspicious, exit2. I should have built this one about 18 months ago. - Notification hook for long tasks. When Claude sends a notification (the
Notificationevent), I fire acurlcall to a Pushover endpoint so I get a push notification on my phone. Genuinely useful when you kick off a big refactor and go make a cup of tea. - PHP syntax check before bash. On WordPress projects, a quick
php -l $CLAUDE_FILE_PATHSbefore any bash execution. Catches fatal syntax errors before they break a staging server.
The official Claude Code hooks documentation has a full reference for environment variables available inside hook scripts. Worth bookmarking.
What Hooks Don't Fix
Honesty matters here. Hooks are not a solution to Claude generating logically wrong code. They fix process problems: formatting, linting, type safety, test coverage. If Claude misunderstands your data model and builds the wrong feature, no amount of post-edit linting will catch that.
Hooks also add latency. If your Prettier + ESLint pass takes four seconds, every file edit now takes four seconds longer. On a project with 200 file edits in a session, that's 13 minutes of waiting. Profile your hook commands. Keep them fast. I run --fix variants (which modify files in place) rather than report-only variants precisely because a single fast pass beats a slow pass followed by a second corrective pass.
And they require you to actually think about your project's failure modes upfront. What can go wrong if Claude edits the wrong file? What standards absolutely must be enforced? That thinking is valuable regardless, but it does mean hooks reward experienced developers more than beginners.
Setting Up Hooks: The Step-by-Step
For anyone starting from zero:
- Create a
.claude/folder in your project root if it doesn't exist. - Add a
settings.jsonfile with your hooks configuration (structure shown above). - For anything more than a one-liner, write a separate shell script (
.claude/hooks/your-script.sh),chmod +xit, and call it from the config rather than inlining the command. - Test by running
claudein your project and deliberately triggering the hook condition. Read what comes back in the session context. - Check the hook execution logs at
~/.claude/logs/if something isn't firing as expected.
The Anthropic developer documentation covers the full settings schema. And if you're thinking about how hooks fit into broader AI coding workflows, Simon Willison's blog is where I'd send anyone who wants to think more carefully about agentic AI tooling in real projects.
One thing I got wrong initially: I put all my hooks in the global ~/.claude/settings.json and then wondered why my PHP hooks were firing on JavaScript projects. Project-level settings override global ones. Put stack-specific hooks in the project's .claude/settings.json and save your global config for things that should apply everywhere (like the audit log and the notification hook).
FAQ
Do Claude Code hooks work on Windows?
The hook commands run in whatever shell your system uses. On Windows that defaults to PowerShell or CMD, which means bash-style scripts won't work natively. WSL2 is the practical answer here. I'm on macOS and Ubuntu on my dev boxes, so I haven't hit this personally, but Anthropic's docs note the shell dependency explicitly.
Can hooks access Claude's conversation context?
Not directly. Hooks run as shell commands and receive environment variables like CLAUDE_FILE_PATHS and CLAUDE_TOOL_NAME, but they don't get the full conversation transcript. What they can do is write output to stdout, which Claude reads as context after the hook runs.
Will hooks slow down my Claude Code sessions noticeably?
Depends entirely on what your hooks do. A php -l syntax check on a single file is under 100ms. Running your full Jest suite on every file edit would be maddening. Keep individual hook commands under two or three seconds and you'll barely notice them.
Are hooks safe to use on production environments?
I'd put that question the other way round: are you running Claude Code directly on production? If yes, hooks are the least of your concerns. Use them on staging, use the PreToolUse blocking pattern to protect sensitive directories, and keep Claude away from production databases entirely.
What's the difference between project-level and global hooks?
Global hooks live in ~/.claude/settings.json and apply to every Claude Code session on your machine. Project-level hooks live in .claude/settings.json inside a specific project and only fire when you're in that project. Project-level takes precedence where both define the same event.
---
The honest summary: hooks are not glamorous. Nobody's going to write a blog post about the beautiful architecture of a shell script that runs Prettier. But they're the difference between Claude Code being a prototype toy and it being something you'd actually trust on client work. I should have set them up on day one. You probably should too.