Skip to content
Claude Code Hooks Explained: Events, Exit Codes and Four Hooks Worth Copying
← All postsAI Tools

Claude Code Hooks Explained: Events, Exit Codes and Four Hooks Worth Copying

November 6, 2026·FixMyPrompt Team·9 min read
Share

What Claude Code hooks are, where they go, how exit code 2 blocks a tool call, and four working examples: auto-format, block rm, protect .env and load context at session start.

#claude code hooks#claude code hooks examples#sessionstart hook#pretooluse hook#claude code settings.json#claude code plugin

An instruction in CLAUDE.md is a request. Claude reads it and usually follows it. A hook is different. It is a command that Claude Code runs itself at a fixed point in the session, every time, whether or not the model remembers to. When you need something to happen without exception, such as formatting every edited file or refusing every rm, a hook is the tool.

What a hook is

A hook is a shell command (or an HTTP call, an MCP tool call, a prompt or a subagent) tied to an event. When the event fires, Claude Code runs the command and passes it a JSON description of what is happening on stdin. The command can let the action go ahead, block it, or add text for Claude to read.

The events you will use most:

EventWhen it fires
SessionStartA session starts, resumes, clears or compacts
UserPromptSubmitYou send a prompt, before Claude sees it
PreToolUseBefore a tool call runs. It can block the call
PostToolUseAfter a tool call succeeds
StopClaude finishes its answer
PreCompactBefore the conversation is compacted

There are many more, including SubagentStop, Notification, FileChanged and SessionEnd. The full list is in Anthropic's hooks reference.

Where hooks go

Hooks live in a settings file or a plugin. Where you put one decides who it applies to:

LocationApplies to
~/.claude/settings.jsonAll your projects, on your machine only
.claude/settings.jsonThis project, committed for the team
.claude/settings.local.jsonThis project, just for you
A plugin's hooks/hooks.jsonAnyone with the plugin enabled

Hooks from different files add up. A project hook does not replace your personal ones. Type /hooks in a session to see everything that is active.

Exit codes decide what happens

Your hook's exit code is its answer:

  • Exit 0 means go ahead. If your hook printed a JSON object, Claude Code applies its fields.
  • Exit 2 means block. On events that can block, such as PreToolUse and UserPromptSubmit, the action stops, and whatever you wrote to stderr becomes the reason Claude sees.
  • Any other exit code is a non-blocking error. You get a notice in the transcript and the action goes ahead anyway.

That third rule catches people out. A safety hook that crashes with exit code 1 does not block anything. If a hook is meant to stop something, make sure its failure path exits 2.

Hook 1: format every file Claude edits

This runs Prettier after every edit. Put it in .claude/settings.json:

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

The matcher limits the hook to the edit and write tools. The command reads the JSON input with jq, takes the edited file's path and formats it. You need jq installed.

Hook 2: block rm commands

Save this as .claude/hooks/no-rm.sh and make it executable:

#!/bin/bash
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

Then register it on the Bash tool:

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

When Claude tries rm -rf build, the call never runs. Claude gets the stderr line as the tool's error, so it knows why and can try another way.

Hook 3: keep Claude out of your .env files

The same pattern protects files. This one blocks edits to anything that looks like an env file:

#!/bin/bash
file=$(jq -r '.tool_input.file_path // empty')

case "$file" in
  *.env|*.env.*|*/.env)
    echo "Blocked: $file holds secrets. Ask before changing it." >&2
    exit 2
    ;;
esac

exit 0

Register it under PreToolUse with the matcher Edit|Write. A rule in CLAUDE.md that says "never touch .env" is good to have too, because it tells Claude why. The hook is what makes it certain.

Hook 4: load context when a session starts

SessionStart is the one event where plain stdout becomes context Claude reads. That makes it the natural place to load anything that changes between sessions, such as the issues you have open or notes saved from the last session.

Here is a short version of the hook the FixMyPrompt plugin uses. It tells Claude which folder it is in and asks it to look up the saved memories for that folder before it starts work:

// hooks/session-start.mjs
import path from "node:path";

const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const name = path.basename(path.resolve(dir));

const additionalContext =
  `This folder is ${name}. Before you start work, call the fmp mem_index tool ` +
  `with project "${name}" and use what it lists.`;

process.stdout.write(
  JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext } }) + "\n",
);
process.exit(0);

And the plugin's hooks/hooks.json:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

A few details are worth copying. The hook never exits with an error, because a broken context loader should not stop a session from starting. It has a timeout, because SessionStart runs on every session and Claude's first answer waits for it. And it fires after a compact as well as at startup, so the context comes back after a long session is summarized.

The full hook is in the fmp-plugins repo if you want to read it. It is the piece that makes FixMyPrompt Memories work in Claude Code: whatever the agent solved last time is listed at the start of the next session, so it does not solve it again.

When to use a hook and when to use CLAUDE.md

Use CLAUDE.md for guidance, including the reasons behind your rules. Claude reads it and applies judgment.

Use a hook when an exception would cost you: secrets, destructive commands, formatting that must be consistent, or context that has to be fresh every session.

Most projects want both. If your CLAUDE.md has grown long or contradicts itself, the free CLAUDE.md checker shows what it costs every session and which guardrails it is missing.

Try it on the prompt that sent you here

Paste the prompt that misbehaved and see what is weak in it.

The weak spots and fixes are tuned to this goal.

0 of 5,000 characters

No prompt handy? Try one:

Found this useful? Share it.

Share

Read next

Run a free QA on your own prompt

Get a structured score, specific issues, and a rewritten prompt in seconds.

Run free QA