Claude Code Hooks: Practical Guide to Automating Tasks

I spent an embarrassing amount of time manually reviewing every file Claude edited on my machine before I discovered Claude’s code hooks. Once I set up a single PostToolUse hook that ran my linter automatically after every write, I never had to babysit that step again. Hooks are one of those features that sit quietly in the documentation until you actually need them, and then you wonder how you worked without them.

At their core, Claude code hooks are user-defined commands, HTTP endpoints, or AI prompts that fire automatically at specific points during a Claude Code session before a tool runs after it completes. When the session ends. You set the trigger, decide the action, and Claude Code handles it from there.

I’ll go through what hooks are, which ones you’ll actually end up using, how to set them up without running into issues, and what a real setup looks like in practice

[IMAGE: Claude Code hooks configuration in a settings.json file showing PreToolUse and PostToolUse claude code hooks setup]

What Claude Code Hooks Actually Do

Hooks run at specific points during a Claude Code session. You can think of them as checkpoints, places where something can be allowed, blocked, or logged, depending on how you’ve set things up.

Whenever Claude runs a tool, processes a prompt, or finishes a response, those hook events get triggered.

If you’ve configured a hook for that event, your command or script runs. If not, nothing happens. The whole system is opt-in and stays silent until you configure it.

The hook receives context about what just happened, the tool name, the command it ran, and the file it wrote, delivered as JSON. Your script reads that, decides what to do, and communicates the result back through exit codes and output. It’s a clean, practical interface once you get your head around it.

What makes this genuinely useful is the blocking capability. Some events, like PreToolUse, let a hook deny an action before it executes. That’s not just logging, that’s real control over what Claude does on your machine.

The Four Types of Claude Code Hooks

Before diving into events, it helps to understand the four hook handler types. They all receive the same JSON input, but they respond differently.

Command hooks (type: "command") run a shell script. This is the most common type and the most flexible. Your script gets JSON on stdin, does whatever it needs to do, and exits with a code that tells Claude how to proceed.

HTTP hooks (type: "http") send the event’s JSON as a POST request to a URL you specify. Useful when you have an existing service that needs to know what Claude is doing, like a logging endpoint or a webhook.

Prompt hooks (type: "prompt") send a natural language prompt to a Claude model Haiku by default and let the model decide whether to allow or block an action. You write something like “evaluate whether this bash command looks destructive,” and the model returns a yes or no.

Agent hooks (type: "agent") go a step further. They spawn a subagent that can actually read files, search code, and investigate your project before returning a decision. If you need to verify that tests pass before allowing Claude to mark a task complete, an agent hook can run the test suite itself.

Hook Events Worth Knowing

The Claude Code Hooks That Block Actions

The most powerful events are the ones that can stop something before it happens; preToolUse fires before any tool call executes and is arguably the event you’ll use most. You can match it specifically to the Bash tool, the Write tool, or any MCP tool and either allow, deny, or ask the user to confirm.

A practical example: I have a PreToolUse hook on Bash that checks for rm -rf in any command Claude tries to run. If it finds the pattern, the hook exits with code 2 and sends an error message back to Claude explaining why the command was blocked. No drama, no accidents.

PermissionRequest works in a similar way, but it triggers right before a permission prompt shows up. You can use hooks here to automatically approve commands you already trust, so you’re not asked every single time.

Events for Logging and Quality Checks

PostToolUse runs after a tool finishes. That’s usually where I hook in things like a linter. Claude updates a file, the hook kicks in, the linter checks it, and any issues get sent back as context so Claude can fix them.

At that point, the tool has already run, so you’re not blocking anything, but you can still guide what happens next.

Stop triggers when Claude is about to wrap up its response. If you return exit code 2 there, it basically tells Claude it’s not done yet. That’s useful if you want to enforce some kind of final check before everything settles.  There’s a stop_hook_active field in the input that tells you if a Stop hook has already triggered this turn, which prevents infinite loops.

SessionStart and SessionEnd are your setup and teardown points. I use SessionStart to print the current Git branch and any open issues into Claude’s context automatically, so I don’t have to paste them manually every time.

Here’s a summary of the events and what control they offer:

EventWhen It FiresCan Block?
PreToolUseBefore tool executesYes
PermissionRequestBefore permission dialogYes
PostToolUseAfter tool succeedsNo (feedback only)
StopWhen Claude finishesYes
SessionStartSession begins or resumesNo
UserPromptSubmitBefore Claude processes promptYes
TaskCompletedTask marked as completeYes

How to Configure Claude Code Hooks

Hooks live in JSON settings files. Where you put the file determines who the hook applies to.

  • ~/.claude/settings.json : applies to all your projects
  • .claude/settings.json : applies to the current project and can be committed to the repo
  • .claude/settings.local.json : project-specific, gitignored

There are basically three parts to it. You choose the event, add a matcher so it only runs when it should, and then define what the handler actually does.

For example, here’s a simple PostToolUse hook that runs a lint check in your settings:

The matcher field is a regex that filters by tool name. "Edit|Write" matches either. "mcp__.*" matches all MCP tools. Omit the matcher entirely, and the hook fires on every occurrence of that event.

Type /hooks inside Claude Code to open a read-only browser of every configured hook. It shows the event, the matcher, the source file, and the full command. I check this whenever something behaves unexpectedly; it’s the fastest way to confirm a hook is actually registered.

Running Hooks in the Background

Some tasks don’t need to block Claude if you want to run a test suite after every file write, but don’t want Claude sitting idle while it runs, set "async": true on the command hook.

Async hooks receive the same JSON input, run as background processes, and deliver their output as context on the next conversation turn. The action Claude took has already happened, so async hooks can’t block anything, but they can report results back and keep Claude informed.

I use this for longer builds. Claude writes a file, the build starts in the background, and a few minutes later, Claude gets a message saying whether it passed or failed. No waiting, no blocking. [INTERNAL LINK: Claude Code subagents and agent teams overview]

One thing to watch for is if your shell prints anything on startup, like a greeting or a current directory message, it can mess with JSON parsing in hooks.

It’s better to keep hook scripts clean and send any debug output to stderr instead of stdout.

Security Basics You Should Not Skip

Hooks run with your full user account permissions. That means a poorly written hook can delete files, expose environment variables, or do anything else your account can do. This isn’t a reason to avoid hooks, but it is a reason to be deliberate.

A few rules I follow without exception. Always quote shell variables "$VAR", not $VAR. Never trust input data without validating it first. Check for path traversal patterns like .. before using any file path from the hook input. Use absolute paths in hook commands and reference $CLAUDE_PROJECT_DIR for project-relative paths.

The ~/.claude/settings.json file is not committed to repos, which is good; it’s where I keep hooks that depend on personal credentials or local tooling. Project-level hooks in .claude/settings.json go in the repo, so teammates get the same behavior automatically.

What Good Hooks Look Like in Practice

The hooks that have worked best for me are the simple ones. Each one does a single job a PreToolUse hook to catch risky Bash commands, a PostToolUse hook to run a linter after changes, and a Stop hook to check for leftover TODO’s.

Whenever I’ve tried to bundle too much into one script, it’s fallen apart pretty quickly. It’s easier to keep things split one script per job, simple matchers, clear exit codes.

And when something breaks, I just run claude --debug and check the hook logs. You can see which hooks ran, what they returned, and any output they produced. Usually enough to figure things out without spending forever on it.

The Claude code hooks system rewards specificity. The more precisely you match events and tools, the more reliable your automation becomes.

Wrapping Up: Start Small, Then Build

The biggest mistake I see people make with Claude code hooks is trying to automate everything at once. Start with one hook. A PostToolUse on Write that runs your linter. A PreToolUse on Bash that logs commands to a file. Get comfortable with the input format, the exit codes, and the settings structure before layering in more.

Once you have a working hook, the pattern clicks, and the rest comes naturally. You start seeing every friction point in your Claude Code workflow as a candidate for automation. That’s the real value of Claude code hooks, not any single hook, but the habit of removing manual steps from your process one at a time.

Pick the event that matters most to your workflow right now and write your first hook today. The documentation at code.claude.com covers every event and input schema. Everything you need is there.

Frequently Asked Questions

Do I need to know how to code to use Claude Code hooks?

You need a basic comfort with the command line and reading JSON, but you don’t need to be a developer. The simplest hooks are one-line shell commands, something like echo "file changed" >> ~/log.txt. From there, you add complexity as needed. If you can write a basic bash script, you can write a useful hook. The PreToolUse event with a simple exit code 0 or exit code 2 is the most common starting point, and it’s genuinely not complicated once you’ve read the input format once. The official docs show annotated examples for each event, which helps a lot for the first few setups.

What happens if a hook crashes or exits with an unexpected code?

Any exit code other than 0 or 2 is treated as a non-blocking error. Claude Code logs the stderr output in verbose mode but continues running. So a buggy hook won’t bring down your session; it just fails silently unless you have verbose mode on. Exit code 2 is the specific signal for “block this action,” and exit code 0 means “proceed.” If you want to debug a misbehaving hook, run claude --debug and look for the hook execution entries. They show the matched hook, the exit code, and any output. You can also use the /hooks command inside Claude Code to confirm whether your hook is even being registered correctly.

Can hooks access environment variables from my machine?

Yes, command hooks run in Claude Code’s environment, which inherits from your shell. Standard variables like HOME and PATH are available. If you need to use a specific variable in an HTTP hook header, list it in the allowedEnvVars field to allow interpolation, for example, to pass an API token in an Authorization header. For sensitive variables, avoid printing them to stdout since hook output can appear in logs. The $CLAUDE_PROJECT_DIR variable is particularly useful because it always points to the project root regardless of the current working directory when the hook fires.

Leave a Comment

Your email address will not be published. Required fields are marked