Skip to content
Moeed Rajpoot

Claude Code Hooks: A Friendly Guide for Beginners

Hooks let Claude Code run small commands before and after it does things. This guide explains what they are, how to set them up, and the few mistakes to avoid.

By Moeed Rajpoot · · 8 min read

If you have started using Claude Code on a real project, you may notice that some things you want it to do every time get forgotten. You may have asked it to format your files after editing, or to skip risky commands, and it does well for a while and then drifts.

Hooks are the simple fix for this.

A hook is just a small shell command that the Claude Code app runs for you when something happens. Before a tool runs, after a file gets edited, when the session ends, and so on. Because the app runs them and not the model, hooks are reliable. The model cannot forget them.

This guide walks through hooks step by step. By the end you will have a working format on save hook, you will know all the events you can hook into, and you will know the few small mistakes that trip up most people the first time.

A hook is a shell command that runs automatically when an event happens in Claude Code. It lives in settings.json, it runs in your terminal, and it is the most dependable way to make Claude Code follow a rule every time.

What hooks are good for

Hooks are useful when you want something to happen every single time, with no exceptions. A few examples that real teams use:

  • Run Prettier or Black on a file the moment Claude saves it.
  • Block dangerous shell commands before they run.
  • Run your tests when a session ends, so nothing broken slips through.
  • Add small bits of context to every prompt, like the list of open issues.
  • Send a quick message to Slack when a long task finishes.

If you have ever written something like please always do X in your CLAUDE.md file and felt that the model ignores it half the time, that is a sign you need a hook for that rule instead.

Where hooks live

Hooks are written inside a file called settings.json. There are three of these and they layer on top of each other:

~/.claude/settings.json              your personal settings
<repo>/.claude/settings.json         settings shared with your team
<repo>/.claude/settings.local.json   personal overrides (kept out of git)

The order is simple. Project settings beat user settings. Local settings beat both. So you can ship one shared .claude/settings.json for the whole team, while keeping your own quirks in settings.local.json.

Your first hook in five minutes

Here is a small starter that runs Prettier on any file Claude edits or writes. Drop this into .claude/settings.json in any JavaScript or TypeScript project.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=\"$CLAUDE_TOOL_INPUT_file_path\"; case \"$FILE\" in *.ts|*.tsx|*.js|*.jsx|*.json|*.md) npx prettier --write \"$FILE\" 2>/dev/null ;; esac"
          }
        ]
      }
    ]
  }
}

What this does, in plain words: every time Claude finishes editing or writing a file, the hook checks if the file is something Prettier knows about. If it is, the hook runs Prettier on it. From now on, Claude cannot leave the codebase unformatted.

The piece that says $CLAUDE_TOOL_INPUT_file_path is how the app passes the file name into your hook. Each tool has its own arguments, and you can read them inside the hook with these environment variables.

All the events you can hook into

You do not need to memorize this list. Just keep it nearby for when an idea comes up.

EventWhen it firesA common use
PreToolUseBefore a tool runsBlock a risky command
PostToolUseAfter a tool finishesFormat a file, run a linter
UserPromptSubmitWhen you press enter on a promptAdd context to the prompt
StopWhen the session endsRun tests, save a draft commit
SubagentStopWhen a sub agent finishesCollect its results
NotificationWhen the app shows a messageForward it to Slack

Of these, the two you will use most are PreToolUse and PostToolUse. They cover almost every workflow rule.

Blocking a risky command

This is the hook that has saved me the most stress. It tells Claude Code to refuse a few specific shell commands.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=\"$CLAUDE_TOOL_INPUT_command\"; case \"$CMD\" in *\"rm -rf\"*|*\"--no-verify\"*|*\"git push --force\"*) echo \"Blocked: $CMD\"; exit 2 ;; esac"
          }
        ]
      }
    ]
  }
}

When a PreToolUse hook exits with status 2, the app blocks the tool call and tells the model what happened. The model then knows what it tried, sees the error, and picks a different path. Any other non zero exit is treated as a normal error and the call still goes through, so use exit 2 only when you really mean do not run this.

A small safety note. Never paste $CLAUDE_TOOL_INPUT_* straight into a shell expression without quotes. The model can put anything in there. Use case matching or fixed string searches instead of fancy patterns.

Adding context to every prompt

This one is a small trick that surprises people the first time they see it. You can have a hook run when you submit a prompt, and whatever it prints gets added to your message before Claude reads it.

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "gh issue list --limit 5 --json number,title --jq '.[]| \"#\\(.number) \\(.title)\"'"
          }
        ]
      }
    ]
  }
}

Now every time you ask Claude something, it sees the latest five open issues from GitHub at the top of your message. You can do the same with your task list, your last few git commits, or any internal note you want the model to be aware of.

A clean way to organize hooks

Once you have more than two or three hooks, the JSON gets noisy. The pattern most teams settle on is to keep small shell scripts in .claude/hooks/ and just call them from the JSON.

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": ".claude/hooks/block-dangerous.sh" }] }
    ],
    "PostToolUse": [
      { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": ".claude/hooks/format.sh" }] },
      { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": ".claude/hooks/typecheck.sh" }] }
    ],
    "Stop": [
      { "hooks": [{ "type": "command", "command": ".claude/hooks/run-tests.sh" }] }
    ]
  }
}

Each script becomes a small file the team can review and improve. You can also write hooks in Python or Node if shell is not your favorite language. The command field is just a normal shell line, so anything you can run from your terminal is fair game.

When a hook is the right tool

There are three ways to teach Claude Code to do something. They are not equal.

ApproachHow reliableWhen to use it
Memory or CLAUDE.md notesLow. The model can ignore it.Style preferences and gentle context.
Slash commandsMedium. You have to remember to call it.A task you do often by hand.
HooksHigh. The app runs them every time.Anything that must happen, every time.

A simple rule of thumb: if you find yourself adding a sentence to CLAUDE.md that starts with the word always, it probably wants to be a hook.

Common questions

Do hooks run for every tool, including read only ones? Only the ones you ask for. The matcher field decides. If you set "matcher": "Edit|Write", the hook only fires for those two. Leave matcher out and it fires for every tool.

Can a hook send something back to the model? Yes. Anything a PreToolUse or UserPromptSubmit hook prints to standard output gets added to the conversation as extra context. PostToolUse output shows up in the transcript.

Can hooks be written in Python? Yes. The command field runs in your shell, so python .claude/hooks/lint.py works the same as a bash one liner. Use whatever language you are fastest in.

Is it safe to commit hooks to git? Yes, and it is the recommended pattern. Commit .claude/settings.json and any scripts in .claude/hooks/. Keep secrets and personal preferences in .claude/settings.local.json instead, and add that file to your .gitignore.

Where to go next

Hooks pair well with two other Claude Code features.

Once you have written three or four hooks, the editor starts to feel less like a stranger and more like a tool that knows your team. That is the whole point. Spend an hour on hooks today, and your future self will quietly thank you for the next year.