AI Coding Tools

What Are Claude Code Hooks? Examples & When to Use (2026)

Aug 14, 202613 min read

Claude Code hooks are shell commands (or HTTP/MCP calls) you configure in settings.json that run automatically at specific moments in a Claude Code session - for example, before a tool runs, after a file is edited, or when Claude finishes a turn. The three core events beginners need are PreToolUse (can block dangerous commands), PostToolUse (auto-format code), and Stop (send a "done" notification). Because hooks run with your full user permissions and no sandbox, review them carefully before turning them on.

Claude Code ships fast, so the list of events keeps growing.

What are hooks in Claude Code?

Hooks in Claude Code are commands you define ahead of time that Claude Code runs automatically at fixed points in the lifecycle of a session. Instead of "reminding" Claude to do something and hoping it remembers, a hook turns that behavior into something deterministic: when the right moment arrives, it runs - regardless of whether the model "feels like it."

If you have ever used Git hooks (like a pre-commit that runs a linter before every commit), this idea will feel familiar. The only difference is that these hooks attach to the lifecycle of an AI coding agent instead of Git. When Claude is about to run a tool, has just finished editing a file, or ends a response turn, Claude Code checks whether any hook is registered for that event and runs it.

The biggest strength of a hook is that it is deterministic and can block. A line in your CLAUDE.md that says "remember to run Prettier after editing files" is only a suggestion - the model can forget. A PostToolUse hook, on the other hand, runs Prettier every single time, no exceptions. With PreToolUse, a hook can even refuse an action before it happens - for example, blocking a dangerous rm -rf.

This makes hooks the core tool for automating Claude Code: formatting code, running tests, logging activity, sending notifications, or building safety guardrails. In this article I walk through how hooks work via settings.json, three copy-paste examples, when you should (and should not) use them, and a safety section that most guides skip.

How do hooks work? (settings.json)

Hooks are declared in your settings.json file under the hooks key. The structure nests three levels deep: event name -> list of matchers -> list of hooks to run. Concretely, that is hooks > EventName > [{ matcher, hooks: [{ type, command }] }]. Here is a minimal settings.json that actually works:

{
 "hooks": {
 "PostToolUse": [
 {
 "matcher": "Edit|Write",
 "hooks": [
 {
 "type": "command",
 "command": "npx prettier --write \"$CLAUDE_FILE_PATHS\""
 }
 ]
 }
 ]
 }
}

Read it from the inside out: when the PostToolUse event fires, Claude Code compares the matcher (Edit|Write) against the name of the tool that just ran; if it matches, it executes each command in the hooks array.

Three configuration scopes - this matters because it decides where a hook applies and whether it gets committed to Git:

FileScopeGitUse for
~/.claude/settings.jsonMachine-wide (global)Not committedPersonal hooks you want on every project
.claude/settings.jsonPer projectCommittable, shared with the teamShared project hooks (format, test)
.claude/settings.local.jsonPer project, just youGitignored (by default)Sensitive hooks with private tokens/paths

Plugins can also ship their own hooks through their hooks/hooks.json file.

The type field supports five kinds: command (run a shell command - by far the most common), http (call a URL), mcp_tool (call a tool on an MCP server - see also what MCP is and how to use it), prompt, and agent. Throughout this article I focus on command because it is copy-paste ready and covers 90% of what you will need. (Getting the settings.json hooks syntax right is a prerequisite - invalid JSON means the hook silently never runs.)

The main event types (lifecycle)

You do not need to memorize all of them. For getting started, just the handful below cover most situations:

EventFires whenCan block?Typical use
PreToolUseBefore Claude runs a toolYesBlock dangerous commands, force confirmation
PostToolUseAfter a tool succeedsNoFormat code, run tests, log
UserPromptSubmitWhen you submit a promptYesInject context, validate input
StopWhen Claude finishes a turnNoSend a "done" notification
SessionStartWhen a new session opensNoLoad environment variables, log
NotificationWhen Claude emits a notificationNoRoute notifications to another channel

2026 update (info gain): a lot of older guides still list only the four classic events. In reality Claude Code now has more than 30 lifecycle events, adding ones like PostToolUseFailure, SubagentStart/SubagentStop, PreCompact/PostCompact, SessionEnd, and more, per Anthropic's official documentation (code.claude.com/docs/en/hooks, accessed 2026-08-09). But do not worry - as a beginner you only need the 3-4 core events above; the rest are for advanced scenarios.

The matcher decides which tool a hook applies to. Four common forms: match a single tool exactly ("Bash"), match several tools with a pipe ("Edit|Write"), a regex ("mcp__.*" to catch every MCP tool), and empty or "*" meaning match everything. Let's compare PreToolUse and PostToolUse in the examples below.

Example 1 - PreToolUse: block dangerous commands

This is the most impressive use case and also the most practical safety guardrail. The idea: before Claude runs any Bash command, a hook inspects the command; if it spots a dangerous pattern like rm -rf, the hook refuses and never lets the command run.

PreToolUse has two ways to block. The "clean" way is to print JSON with a permissionDecision set to one of three values: "allow" (run it, skip the confirmation step), "deny" (block entirely), or "ask" (force a prompt). The quick-and-dirty way is to use an exit code: the script exits with exit 2 to block - in which case whatever you wrote to stderr is sent back to Claude so it knows why it was blocked. The config:

{
 "hooks": {
 "PreToolUse": [
 {
 "matcher": "Bash",
 "hooks": [
 {
 "type": "command",
 "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-danger.sh"
 }
 ]
 }
 ]
 }
}

And the script .claude/hooks/block-danger.sh:

#!/usr/bin/env bash
# Read the JSON payload from stdin, pull out the command Claude wants to run
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')

if echo "$command" | grep -Eq 'rm[[:space:]]+-rf|git[[:space:]]+push[[:space:]]+--force'; then
 echo "Blocked: command matches a dangerous pattern ($command)" >&2
 exit 2 # exit 2 = block, stderr is sent back to Claude
fi
exit 0 # exit 0 = allow it to continue

The result: when Claude tries to run rm -rf build/, the hook catches it, returns exit 2, the command never runs, and Claude receives a message explaining why. This is exactly what a CLAUDE.md rule cannot guarantee - a rule is only a soft suggestion, while a hook is a hard guardrail. If you want to tighten permissions at a higher level, read more on permissions and safe configuration in Claude Code.

Example 2 - PostToolUse: auto-format code

One of the first hooks I turn on in every project: automatically run the formatter after each time Claude edits a file. No more messy diffs from a missing space or a wrong line break. Because this is PostToolUse (it runs after the tool succeeds), it does not block anything - it only cleans up.

{
 "hooks": {
 "PostToolUse": [
 {
 "matcher": "Edit|Write",
 "hooks": [
 {
 "type": "command",
 "command": "cd \"${CLAUDE_PROJECT_DIR}\" && npx prettier --write \"$CLAUDE_FILE_PATHS\""
 }
 ]
 }
 ]
 }
}

The matcher Edit|Write catches both file-editing tools. For a Python project, swap the command for black "$CLAUDE_FILE_PATHS" or ruff format. A few handy environment variables Claude Code passes into a hook:

  • ${CLAUDE_PROJECT_DIR} - the absolute path to the project root, so the command runs in the right directory.
  • $CLAUDE_FILE_PATHS - the path(s) of the file(s) just touched, so you format the right file instead of the whole repo.
  • You can also always read the full JSON payload from stdin (as in Example 1) to get tool_input details.

Tip: keep the command light and fast. A PostToolUse hook runs after every file edit, so a slow formatter will drag the whole session down.

Example 3 - Stop: notify when Claude is done

When you hand Claude a long task and switch to something else, it is easy to forget to check back. A Stop hook runs when Claude finishes a response turn - perfect for firing off a notification. Here is an example using ntfy to push to your phone:

{
 "hooks": {
 "Stop": [
 {
 "hooks": [
 {
 "type": "command",
 "command": "curl -s -d \"Claude Code finished the task\" ntfy.sh/your-topic-name"
 }
 ]
 }
 ]
 }
}

No matcher is needed because Stop is not tied to any tool. On macOS you can swap in osascript -e 'display notification "Done!" with title "Claude Code"'; on Linux use notify-send. Small, but incredibly handy when you juggle several things at once.

When you should - and should NOT - use a hook

Hooks are powerful, but they are not the tool for everything. The line is simple: hooks are for things that must always run and are deterministic - formatting, testing, blocking commands, logging. If what you need is behavioral guidance or a capability, there is a better-suited tool.

What you want to solveThe right tool
Something that must run every time, deterministically (format, test, block commands)Hook
Steering Claude's style/code conventionsA rule in CLAUDE.md
An action you trigger yourself when neededSlash commands in Claude Code
Packaging a reusable capability (instructions + scripts)Skills in Claude Code

An easy example to get wrong: "remind Claude to always write tests" should be a rule in CLAUDE.md, not a hook - because it is soft guidance. But "run the full test suite after editing files in src/" is genuinely a hook, because it is deterministic. If these four concepts still feel blurry, I have a dedicated piece on how skills, subagents, hooks, and MCP differ - that is where the whole picture comes together.

⚠️ Safety notes when using hooks

This is the section most guides skip, yet it is the most important. Per the official docs (code.claude.com/docs/en/hooks, accessed 2026-08-09): hooks run with your full user account permissions and NO sandbox. That means a buggy hook - or a malicious one you copied in by accident - can delete files, leak secrets, or run arbitrary code on your machine, fully automatically and without asking.

A few principles I always follow:

  • Read every hook carefully before enabling it - especially hooks that come from a plugin, a kit, or someone else's repo. Treat it as code running as root.
  • Never hardcode secrets (tokens, API keys) in a command. Read them from environment variables instead of writing them inline.
  • Keep sensitive hooks in .claude/settings.local.json (gitignored) so you do not accidentally commit them to a shared repo.
  • Know the emergency off switch: use disableAllHooks to turn every hook off while debugging or when something looks suspicious. Enterprises can lock things down further with allowManagedHooksOnly and allowedHttpHookUrls.
  • Be careful with a PreToolUse that auto-allows - it skips the confirmation step, which is convenient but removes a layer of protection.

Bottom line: a hook is a sharp knife. Very useful, but you have to hold it the right way. See more common issues and fixes in troubleshooting common Claude Code errors.

Going faster: ready-made hooks and skills from a kit

Writing your own hooks, scripts, and skills for each project takes real effort - especially when you want a consistent set of safety guardrails and workflows. Some kits for Claude Code, like the AgentKit kit for Claude Code, bundle skills, subagents, and workflows so you do not have to build everything from scratch. If you want to see it directly, you can check AgentKit's pricing (20% off via link). To be clear: the basic hooks in this article you can build yourself and do not need to buy anything; a kit is only worth considering when you want a whole ready-made set.

Debugging: why isn't my hook running?

A "silent" hook is the most common problem. Walking this checklist almost always finds the cause:

  • Is the JSON valid? A single trailing comma in settings.json and the whole file fails to load. Run it through a JSON validator.
  • Does the matcher use the correct tool name? Names are case-sensitive: it is Bash, Edit, Write - not bash or edit.
  • Does the script return the right exit code? exit 0 to pass, exit 2 to block (with PreToolUse). Other exit codes may be ignored.
  • Is stdout "clean"? If the hook returns control JSON, stdout must contain only that JSON - stray text will break parsing.
  • Is the script executable? On macOS/Linux remember chmod +x.
  • Is disableAllHooks on? If you turned it off earlier to debug, do not forget to turn it back on.

Frequently asked questions (FAQ)

How is a hook different from a slash command and a skill?

A hook runs automatically at points in the lifecycle (you do not call it by hand). A slash command is an action you trigger yourself when needed. A skill is a packaged capability (instructions + scripts) that Claude loads on its own when the context fits. In short: hook = automatic and deterministic, slash command = manual, skill = a reusable capability.

Do hooks work on Windows?

Yes. Because type: command runs a shell command, you can point it at a PowerShell script (powershell -File .claude\hooks\block-danger.ps1) or use Git Bash/WSL to run a bash script. The command just needs to be valid in your machine's shell.

Do hooks slow Claude Code down?

They can, if the command is heavy. Hooks run synchronously at the moment of the event, so a slow formatter or test suite will stretch out every turn. Keep hooks light, format only the file that just changed ($CLAUDE_FILE_PATHS) instead of the whole repo, and consider moving heavy tests out of PostToolUse.

What is the difference between global and project hooks?

A global hook (~/.claude/settings.json) applies to every project on your machine and is not committed. A project hook (.claude/settings.json) applies only to that project and can be committed so the whole team shares it. The .local.json variant is per project but gitignored, for your own private config.

Can you block dangerous commands with a hook?

Yes, and this is exactly the strength of PreToolUse. The hook inspects the command before it runs and refuses it with permissionDecision: "deny" or exit code 2 - for example, blocking rm -rf or git push --force (see Example 1 above).

Are hooks safe?

Hooks run with your full user permissions and no sandbox, so a buggy or malicious hook can do real damage (delete files, leak secrets). The mechanism itself is safe if you write and review your hooks carefully; the risk comes from enabling an unknown hook without reading it. Always treat a hook as code running with the highest privileges.

Conclusion + next steps

Master the three core events and you can use hooks for almost everything you need: PreToolUse to block dangerous commands, PostToolUse to auto-format/test, and Stop to get notified. Remember the golden rule: hooks run with full permissions and no sandbox - review them carefully before enabling. To go further, read what skills in Claude Code are and slash commands in Claude Code, or step back for the big picture on how skills, subagents, hooks, and MCP differ and when to use each.

J

Jasmine

Author · Jasmine Daily

The writer behind Jasmine Daily - jotting down thoughts, experiences, and everyday moments. Honest, unhurried, imperfect.

Jasmine Daily

There's more waiting to be read.

If this piece spoke to you, browse a few more pages from the journal.

Read next

Related posts