Claude Code Feature

Claude Code Hooks: What They Are and 3 Ready-to-Use Examples

A practical walkthrough of hooks, Claude Code's way of running your own shell commands at specific points in a session.

Hooks are one of the more underused parts of Claude Code: a way to run your own shell command automatically at a specific point in a session, so that a rule gets enforced every time instead of relying on Claude to remember it, or on you to remember to ask. A hook can block a dangerous command before it runs, clean up a file right after it's edited, or load context into a session the moment it starts. This guide covers what a hook actually is, where you configure one, and walks through three complete, working examples you can copy into a real project today.

A Claude Code hook is a shell command, or another handler type like an HTTP call, that runs automatically at a defined point in a session, such as before a tool runs, after a file is edited, or when a session starts. Hooks are configured as JSON in a settings file, either per-project or for every project on your machine, organized by event name and an optional matcher narrowing which tool or situation triggers them. Because a hook always runs when its event fires, it enforces a rule deterministically, unlike asking Claude to remember a rule in its instructions, where it might forget or make an exception.

Published

Free lesson · no account neededMeet ClaudeWhy Claude?6 minStart the free lesson

Key Takeaways

  • A hook is a command that runs automatically at a specific point in a Claude Code session, such as before a tool call, after a file edit, or at session start.
  • Hooks live in a settings.json file, either for one project or for every project on your machine, organized by the event name they respond to.
  • An optional matcher narrows a hook to specific tools, like only Bash calls or only Edit and Write calls, using either an exact name, a list, or a pattern.
  • A hook that exits with a specific error code can block the action it's attached to and tell Claude why, which is stronger than writing the same rule into project instructions.
  • Hooks run outside Claude's permission sandbox: a hook script can do anything your own user account could do, so only add hooks you've read and trust.
  • Run /hooks inside a session to see every hook currently configured and where each one is defined, which is the fastest way to audit what's actually wired up.

Copy and Paste

Prompts You Can Use Today

Block a dangerous Bash command before it runs

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

#!/bin/bash
# .claude/hooks/block-rm.sh (chmod +x)
input=$(cat)
command=$(jq -r '.tool_input.command' <<<"$input")
if [[ "$command" == rm* ]]; then
  echo "Blocked: rm commands are not allowed" >&2
  exit 2
fi
exit 0

Put the JSON in .claude/settings.json and the script next to it. Exiting with code 2 is how a hook blocks the action and sends the reason back to Claude. This simple version only catches commands that literally start with "rm", so treat it as a starting point, not a complete guardrail, and lean on real permission rules for anything that has to be a hard stop.

Auto-format every file Claude edits or writes

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

Add this to .claude/settings.json and every file Claude edits or writes gets run through Prettier right afterward, with no extra step on your part. It needs jq and npx available on your machine, and it runs silently when there's nothing to fix. A PostToolUse hook can't block the edit itself, only react to it, which is the right tradeoff for formatting.

Load repo context automatically at session start

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-context.sh"
          }
        ]
      }
    ]
  }
}

#!/bin/bash
# .claude/hooks/session-context.sh (chmod +x)
branch=$(git -C "$CLAUDE_PROJECT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null)
dirty=$(git -C "$CLAUDE_PROJECT_DIR" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
jq -n --arg ctx "Current branch: ${branch:-unknown}. Uncommitted files: ${dirty}." \
  '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $ctx}}'
exit 0

This runs once when a session starts or resumes and quietly tells Claude which git branch you're on and how many files are uncommitted, before you've typed anything. Keep a SessionStart hook fast, since it runs on every single session start.

The Basics

What a Hook Actually Is

A hook is a command Claude Code runs on its own when a specific thing happens in a session: before a tool is used, after a tool finishes, when a session starts or ends, when a subagent finishes its work, and dozens of other defined moments. Most hooks run a shell command, though other handler types exist for things like calling an HTTP endpoint or routing through an MCP tool, a prompt, or another agent.

The core appeal of a hook over just writing a rule into your project instructions is that a hook always runs. Project instructions are read by Claude and followed on a best effort basis; a hook fires deterministically every time its event happens, whether or not Claude remembers to check. That distinction matters most for anything you actually need enforced rather than merely suggested, like running a formatter after every edit or stopping a specific class of command before it executes.

Hooks are configured as plain JSON, nested under a top-level "hooks" key, organized first by event name and then by one or more matcher groups, each with its own list of handlers to run. You can see every hook currently wired into a session, and where each one came from, by running /hooks inside Claude Code.

Where They Live

Configuring a Hook

Hooks can be set at several different scopes, which determines who it applies to and whether it's shared with a team.

Project settings

A .claude/settings.json file in the project root is meant to be committed, so every teammate working in that repository gets the same hooks automatically.

Personal project settings

A .claude/settings.local.json file in the same project applies only to you, which is the right place for a hook that's a personal preference rather than a team rule.

User-level settings

A settings.json file in your home Claude configuration folder applies across every project you work in on that machine, useful for something you always want regardless of repo.

Plugins and managed policy

A plugin can ship its own bundled hooks, and an organization can set managed policy hooks that apply to everyone on a team, layered on top of whatever a project or user sets individually.

The Shape of a Hook

Events, Matchers, and Handlers

A hook configuration is organized as event name, then matcher group, then a list of handlers. The event name is one of several dozen defined lifecycle points: common ones include PreToolUse and PostToolUse (before and after any tool call), SessionStart and SessionEnd, UserPromptSubmit, and Stop, alongside more specialized events for subagents, worktrees, context compaction, and model switches.

The matcher narrows which situations actually trigger the handler. Leaving it out, or setting it to an empty string or a bare asterisk, matches everything for that event. A simple name like "Bash" or a pipe-separated list like "Edit|Write" matches those specific tools exactly, and anything more complex than a plain name or list is treated as a JavaScript regular expression, which is handy for something like matching every MCP tool from one particular server.

Each handler in the list has a type, almost always "command" for a shell command in practice, plus the command itself and a handful of optional fields: a timeout, a human-readable status message shown while it runs, and for tool-related events, an "if" condition written in the same syntax as a permission rule, which is best described as a convenience filter rather than a hard security boundary on its own.

Reading the Result

Exit Codes and Talking Back to Claude

A hook's exit code decides what happens next. Exiting 0 means success; for a handful of events, including UserPromptSubmit and SessionStart, anything the hook printed to standard output is folded into Claude's context automatically. Exiting with code 2 is a blocking failure: whatever the hook wrote to standard error is handed back to Claude as the reason, and for an event like PreToolUse, that's what actually stops the tool call from happening. Any other non-zero exit code is treated as a non-blocking error, meaning the action proceeds anyway and the failure is mostly informational.

Not every event can block, which matters for how you design a hook. PostToolUse runs after the fact, so it can tell Claude something went wrong but can't undo the tool call that already happened; SessionStart similarly cannot block a session from starting. If you need a hook to actually stop something, attach it to an event that happens before the action, like PreToolUse, rather than one that only observes after.

A hook can also return structured JSON on standard output instead of plain text, which gives it finer control, like a PreToolUse hook explicitly setting a permission decision of allow, deny, or ask, or a SessionStart hook adding a specific block of additional context rather than whatever happened to be printed. This structured form is worth reaching for once a plain exit code and some stderr text isn't expressive enough for what you're trying to do.

A Word of Caution

Hooks Run Outside the Sandbox

It's worth being direct about what a hook actually is: a real program running with your own user permissions, outside of whatever tool-permission rules you've set for Claude itself. Claude Code's permission system governs what Claude's own tool calls are allowed to do; it doesn't sandbox a hook's own script, which can read, write, or send anything your own account can. A hook you didn't write and haven't read is exactly as risky as running any other unreviewed script on your machine, so the same caution that applies to installing a browser extension or running a random shell one-liner applies here too.

That's also why a hook is a better fit for enforcing something you're confident about, like a formatting rule or a specific command you genuinely never want run automatically, rather than for anything where you'd rather Claude explain its reasoning before acting. Reach for a hook when you want a rule enforced the same way every time, and reach for plain project instructions, or an explicit back-and-forth with Claude, when the situation actually needs judgment.

Hooks Are Part of a Bigger Skill: Setting Up a Project Properly

Knowing when to reach for a hook instead of a plain instruction, and writing instructions Claude actually follows, are both things Nightschool AI's free lesson on setting up a Claude Code project covers from the ground up.

Frequently Asked Questions

What is a Claude Code hook?

A command, almost always a shell command, that Claude Code runs automatically at a defined point in a session, such as before a tool call, after a file edit, or when a session starts. Hooks run deterministically, every time their event fires.

Where do I configure a hook?

In a settings.json file, either .claude/settings.json for a project (meant to be committed and shared), .claude/settings.local.json for personal project settings, or a user-level settings file that applies across every project on your machine.

Can a hook actually stop Claude from doing something?

Yes, for events that happen before the action, like PreToolUse. A hook that exits with code 2 blocks the action and sends its explanation back to Claude. A hook attached to an after-the-fact event, like PostToolUse, can't block, only react.

What's the difference between a hook and writing a rule into project instructions?

Project instructions are read and followed on a best effort basis. A hook runs the same way every single time its event happens, regardless of whether Claude remembers to check, which makes it the stronger choice for anything that genuinely needs to be enforced rather than merely suggested.

Are hooks safe to add from the internet without reading them first?

No. A hook is a real script running with your own user permissions, outside Claude's own tool-permission sandbox. Treat an unreviewed hook exactly like any other unreviewed script you'd be asked to run on your machine, and read it before adding it.

How do I see what hooks are currently active in a session?

Run /hooks inside Claude Code. It lists every hook currently configured for that session and where each one is defined, which is the quickest way to check what's actually wired up before trusting it.

What does the matcher field in a hook actually do?

It narrows which situations trigger the handler. Leaving it blank or using an asterisk matches everything for that event; an exact tool name or a pipe-separated list matches those specific tools; anything more complex is treated as a regular expression.

Can a plugin add hooks to my project automatically?

Yes. A plugin can bundle its own hooks that apply once the plugin is enabled, on top of whatever hooks a project's own settings or your personal settings already define.

Enforce the Rules That Actually Matter

The habit of knowing what to automate versus what to ask Claude to use judgment on.

Nightschool AI is an independent learning platform and is not affiliated with, endorsed by, or sponsored by Anthropic. Claude is a trademark of Anthropic, PBC. Looking for Anthropic's official Claude Academy? It's at academy.claude.com.