Claude Code Hooks: Automate Your Coding Workflow Like a Pro
Blog
AIClaude Code

Claude Code Hooks: Automate Your Coding Workflow Like a Pro

Master Claude Code Hooks to automate repetitive tasks, enforce coding standards, and streamline your development workflow.

Damian Demasi

Damian Demasi

Software Engineer & AI systems builder

In this post, I want to break down what Claude Code’s hooks are, how they work, and why they matter. I'll take you from the basics all the way to some practical use cases, so whether you're just getting started with Claude Code or you've been curious about what hooks can do, this post should give you a solid foundation.

So, What Are Hooks?

Think of hooks as a way to tell Claude Code:

"Hey, every time you're about to do this, I want you to also do that."

In more technical terms, hooks are commands that you define and that run automatically at specific moments during Claude Code's lifecycle. They give you deterministic control over what happens during a coding session. Instead of hoping that Claude will remember to format your code or avoid touching certain files (LLMs are probabilistic models), you can set up a hook that guarantees it.

Here's the thing that makes hooks special: they sit between the AI model and the actual execution of actions. When Claude decides it needs to read a file, for example, the flow looks something like this:

  1. The AI model (Claude) decides to call the Read tool
  2. Your PreToolUse hook fires (and can block the action if needed)
  3. The harness (Claude Code) executes the read operation
  4. Your PostToolUse hook fires (you can provide feedback or log the action)
  5. The result is returned to the AI model (Claude)

This means you can intercept, validate, and even block actions before they happen. That's a big deal when you want to enforce project rules or prevent mistakes.

Where Do You Define Hooks?

Hooks live inside JSON settings files. Depending on where you put them, they'll have different scopes:

  • Global hooks go in ~/.claude/settings.json and apply to all your projects. Great for things like desktop notifications.
  • Project hooks go in .claude/settings.json at the root of your project. These can be committed to your repo and shared with your team.
  • Local project hooks go in .claude/settings.local.json, which is gitignored. Perfect for personal preferences you don't want to push to the team.

You can also define hooks inside plugins, skills, and subagent configurations, but for now, let's focus on the settings files as they're the most common approach.

The Anatomy of a Hook

A hook configuration has three layers of nesting, and understanding them is key to setting things up correctly.

First, you choose a hook event: when should this hook fire? For example, PreToolUse fires before a tool executes, and PostToolUse fires after.

Second, you add a matcher: which tool calls should trigger this hook? A matcher like "Bash" means "only fire when Claude uses the Bash tool." You can combine matchers with a pipe character, like "Edit|Write", to match multiple tools.

Third, you define one or more hook handlers: what should actually run when the hook fires? In most cases, this is a shell command.

Here's a minimal example that sends a desktop notification (using the Notification hook event) whenever Claude Code is waiting for your input:

json15 lines
1{
2  "hooks": {
3    "Notification": [
4      {
5        "matcher": "",
6        "hooks": [
7          {
8            "type": "command",
9            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
10          }
11        ]
12      }
13    ]
14  }
15}

The "matcher": "" part means "fire on every notification, no filtering." The "type": "command" tells Claude Code this is a shell command to execute. Simple, right?

The Most Useful Hook Events

Claude Code supports over 20 hook events, which can feel overwhelming at first. The good news is that you can get a lot of value from just a handful of them. Let me walk you through the ones I think you'll use the most.

PreToolUse: The Gatekeeper

This is probably the most powerful hook. It fires right before Claude executes a tool, and it can block the action entirely. This is your chance to validate what Claude is about to do and say "no ✋" if it's something you don't want.

A classic use case is protecting sensitive files. You can set up a PreToolUse hook with a matcher on "Edit|Write" that checks the target file path. If Claude is about to modify your .env file or anything inside .git/, the hook script exits with code 2, and Claude receives feedback explaining why the edit was blocked.

The key thing to remember here is how exit codes work:

  • Exit 0: let the action proceed.
  • Exit 2: block the action. Whatever you write to stderr gets sent back to Claude as feedback.
  • Any other exit code: the action proceeds, but an error notice appears in the transcript.

You can also go beyond simple allow/block by outputting a JSON object to stdout. This lets you return a permissionDecision of "allow", "deny", or "ask" (which prompts the user for confirmation). You can even modify the tool's input parameters before execution using the updatedInput field.

PostToolUse: The Cleanup Crew

This hook fires immediately after a tool has executed successfully. Since the action already happened, you can't block it, but you can react to it. The most popular use case here is auto-formatting (so you can avoid these sorts of disagreements).

Imagine Claude writes or edits a file. A PostToolUse hook with a matcher on "Edit|Write" can extract the file path from the hook's input and pipe it through Prettier, ESLint, or whatever formatter your project uses. Every file Claude touches gets formatted automatically, without you ever having to think about it.

Notification: Never Miss a Beat

The Notification hook fires whenever Claude Code needs your attention, whether it's waiting for permission to run a command or it's been idle for a while. This is incredibly useful when you're multitasking. Set up a notification hook once in your global settings, and you'll always know when Claude needs you.

Stop: Quality Control Before Finishing

The Stop hook fires when Claude finishes responding. What makes this hook interesting is that you can prevent Claude from stopping by exiting with code 2 or by returning "decision": "block" in your JSON output. The reason you provide gets fed back to Claude, and it continues working.

This opens the door to some creative workflows. For example, you could set up a Stop hook that runs your test suite and blocks Claude from finishing if tests are failing. Claude would then receive the test failures as feedback and keep working until things pass.

One important caveat: when writing Stop hooks, always check the stop_hook_active field in the input JSON. If it's true, it means Claude is already continuing because of a previous stop hook. If you don't check this, you might end up in an infinite loop where Claude never stops.

SessionStart: Setting the Stage

This hook fires when a session begins, resumes, or when context is compacted. It's your chance to inject context into Claude's awareness at the start of a conversation. Any text your script writes to stdout gets added to Claude's context.

A particularly useful pattern is using the "compact" matcher. When Claude's context window fills up and compaction happens, important details can get lost. A SessionStart hook with a "compact" matcher can re-inject critical reminders after every compaction, like "use Bun instead of npm" or "always run tests before committing."

Beyond Shell Commands: Other Hook Types

While most hooks are shell commands ("type": "command"), Claude Code supports three other types that are worth knowing about.

Prompt hooks ("type": "prompt") send a prompt to a Claude model (Haiku by default) to make a yes/no decision. Instead of writing a script to check a condition, you describe the condition in natural language, and the model evaluates it. The model returns "ok": true to proceed or "ok": false with a "reason" to block.

Agent hooks ("type": "agent") go a step further. They spawn a subagent that can actually inspect files, run commands, and use other tools to verify conditions. Think of it as a mini Claude that checks the work of the main Claude before it's allowed to finish.

HTTP hooks ("type": "http") send the event data as a POST request to a URL. This is useful when you want an external service to handle the hook logic, like a shared audit service that logs tool usage across a team.

Tips for Getting Started

If you're new to hooks, here are a few things I'd suggest:

Start with the /hooks command. Type /hooks inside Claude Code to open a read-only browser that shows all your configured hooks. It's a great way to verify that your configuration is correct and see where each hook is coming from.

Start small. Don't try to set up 10 hooks on your first day. Begin with a notification hook and maybe a formatter hook. Get comfortable with how they work, and then gradually add more as you identify patterns in your workflow.

Use $CLAUDE_PROJECT_DIR for script paths. When referencing scripts in your hook commands, wrap the path with this environment variable so your hooks work regardless of the current working directory.

Be mindful of matchers. A broad matcher (or an empty one) means your hook fires on every occurrence of the event. That's fine for notifications, but for something like PreToolUse, you probably want to narrow it down to specific tools.

**Debug with --debug-file.** If a hook isn't behaving as expected, start Claude Code with claude --debug-file /tmp/claude.log and tail the log in another terminal. You'll see exactly which hooks matched, their exit codes, and their output.

Key Takeaways

Hooks give you a layer of deterministic control on top of Claude Code's AI capabilities. Instead of relying on the model to always do the right thing, you can enforce rules, automate repetitive tasks, and integrate Claude Code with your existing toolchain.

The beauty of hooks is that they're just shell commands at their core. If you can write a Bash script (or a Python script, or a Node.js script), you can write a hook. And the flexibility of the system, with its matchers, exit codes, and JSON output, means you can build anything from a simple notification to a sophisticated validation pipeline.

If you want to go deeper, I'd recommend checking out the official hooks guide and the hooks reference for the full list of events and their schemas. There's also a complete Bash command validator example in the Claude Code repository that's worth studying.

Have you tried using hooks in Claude Code yet? Which use case are you most excited to try first? Let me know in the comments!

Happy coding! 👋

Continue Reading