Skip to content

Hooks Fundamentals

Hooks inject context into every message — unlike CLAUDE.md which loads once. The hook lifecycle, types, and why this changes everything.

CLAUDE.md is advice. Claude Code reads it, processes it, does its best to follow it. But "best" isn't "always." Sometimes it forgets. Sometimes it decides your rule doesn't apply. That's the nature of advice.

Hooks are not advice. They are deterministic guarantees.

What Hooks Actually Are

Think event listeners for Claude Code. You define them in a settings file, they execute every time a specific event occurs. Not once at session start like CLAUDE.md. Every. Single. Time.

Claude Code has a lifecycle. Session starts. You submit a prompt. It calls a tool. Tool finishes. Session ends. At each of these points you can attach code that runs automatically. No approval prompts. No hoping it remembers.

Why does this matter? Say you want Prettier to run on every file Claude Code edits. You could put "always run Prettier after editing files" in CLAUDE.md. It'll probably do it. Most of the time. With a hook, you attach a shell command to the PostToolUse event matching Edit and Write tools. Formatting happens every time, no exceptions. Claude Code doesn't even "decide" to do it. The hook fires, the command runs. Done.

The Lifecycle

Hooks can fire at these lifecycle points:

SessionStart fires once when a new session begins. Set up env vars, check prerequisites, log things.

UserPromptSubmit fires every time you send a message. Validate input, inject context, block prompts.

PreToolUse fires right before Claude Code calls a tool (Edit, Write, Bash, etc.). Inspect what it's about to do. Exit code 2 blocks the action entirely. Your veto power.

PostToolUse fires right after a tool call completes. Run linters, formatters, tests, whatever.

Notification fires when Claude Code sends a notification. Custom alerting.

Stop fires when Claude Code finishes its response. Summary logging, final test suite, Slack yourself.

SessionEnd fires on session close. Cleanup.

There are more (SubagentStart, SubagentStop, PreCompact, ConfigChange) but the ones above cover 90% of what you will actually build.

Four Flavors

Command runs a shell command. By far the most common. Receives JSON on stdin, communicates back through stdout, stderr, and exit codes.

HTTP sends a POST request to a URL with event data in the body. Good for external services, logging platforms, webhooks.

Prompt makes a single LLM call with a template. Quick classification or decision-making without spinning up a full agent.

Agent spins up a multi-turn sub-agent. The heavy artillery. It can reason, use tools, make decisions on its own before returning a result.

Where They Live

Three levels of settings files:

  • User-level: ~/.claude/settings.json (applies everywhere)
  • Project-level: .claude/settings.json (committed to repo, shared with team)
  • Local: .claude/settings.local.json (repo-specific but not version controlled)

A real config. This runs Prettier on every file Claude Code edits or writes:

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

The matcher is a regex filter. Edit|Write means this hook only fires on those tool calls. No matcher? Fires on every PostToolUse event regardless of tool. You can match on tool names, session types, notification types depending on the event.

Actually, the easiest way to set hooks up is the interactive /hooks menu inside Claude Code. It walks you through everything.

Exit Codes

Simple protocol. Hook receives JSON on stdin, writes output to stdout, feedback to stderr, signals decisions through exit codes.

  • Exit 0 — all good, proceed
  • Exit 2 — block the action. Hard stop. A PreToolUse hook that returns 2 prevents Claude Code from executing that tool call. Period.
  • Anything else — something went wrong, Claude Code logs it and continues

Exit code 2 is the killer feature. Write a hook that checks if Claude Code is about to delete a protected file and block it. Not a polite suggestion. A wall.

Try It

Open Claude Code, run /hooks. Pick PostToolUse, matcher Edit|Write, command type:

jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true

Edit any file through Claude Code. Check it after. Prettier-formatted. Every time. No remembering, no hoping.

Verify with:

cat .claude/settings.local.json

Wrap Up

Hooks are guarantees, not suggestions. They fire at specific lifecycle points, deterministically. PreToolUse (inspect/block), PostToolUse (validate/format), and Stop (finalize) are the three you'll use most. Exit code 2 blocks actions entirely, giving you real veto power. Four types exist (command, http, prompt, agent), and /hooks is the fastest way to set them up. Config lives in settings JSON at user, project, or local level.

Mastering Claude Code

Lesson 41 of 70