← Back to Blog

Claude Code Hooks: A Complete Guide (With a Real Example)

Claude Code hooks are scripts that run automatically on session events. Four events are available: PreToolUse (before a tool runs), PostToolUse (after a tool finishes), Notification (on status messages), and Stop (on session end). Hooks are registered in ~/.claude/settings.json and execute asynchronously. A practical use case is session awareness: a PostToolUse hook calls Claude Haiku every 20 tool uses to generate a human-readable topic for each terminal window, so users can identify what each session is working on.

You come back to your terminal after an hour away. There are 13 windows open. They’re named things like agentdeck_britt-hq_a32909e8. You have no idea what any of them are doing, which ones are finished, or which one was the important one. If you’re not using tmux yet, tmux for Claude Code Users covers the basics.

That’s a real problem. And it’s exactly the kind of problem Claude Code hooks were built to solve, if you know they exist.

This post covers what hooks are, how the event system works, how to write one, and a real example of using hooks to build session awareness so you always know what Claude is doing.


What are Claude Code hooks?

A hook is a script that runs automatically when something happens inside Claude Code. Not when you ask it to, and not when you remember to run it manually. It fires on its own, triggered by an event.

The events are things like: Claude is about to use a tool. Claude just finished using a tool. A new notification came in. The session is about to stop.

You write a shell script (a plain text file with commands your terminal can run), register it to an event, and from that point on, every time that event fires, your script runs. Claude Code handles the triggering. You just write what you want to happen.

This is different from prompts, which tell Claude what to do. Hooks tell the system what to do around Claude, independent of the conversation.


The event system

Claude Code exposes four hook events:

PreToolUse fires before Claude runs a tool. Claude has decided to use something (like bash or write_file) but hasn’t run it yet. This is where you can inspect what it’s about to do, log it, or even block it.

PostToolUse fires after a tool finishes. The tool ran, you have the result. Useful for logging what happened, triggering follow-up actions, or building an audit trail.

Notification fires when Claude Code emits a notification. This could be something you’d normally see as a pop-up or status message.

Stop fires when the session ends. Good for cleanup, summaries, or writing a final log entry.

Each hook gets passed context about what triggered it: which tool was used, what the inputs and outputs were, what the current session state looks like.


How to create a hook

Hooks live in your Claude Code settings file at ~/.claude/settings.json. The format looks like this:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "bash",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/your/script.sh"
          }
        ]
      }
    ]
  }
}

The structure is: event name, then a list of matchers. A matcher specifies which tool (or "*" for all tools) triggers the hook, and what command to run.

The command is anything your shell can execute. A Python script, a bash script, a Node script, a curl call. Whatever you want.

When the hook fires, Claude Code passes JSON to the script via stdin. Your script reads that JSON, does whatever it needs to do, and exits. If it exits with a non-zero status code, Claude Code treats that as an error.

To match all tools instead of just one, use "*" as the matcher:

{
  "matcher": "*",
  "hooks": [
    {
      "type": "command",
      "command": "/path/to/your/script.sh"
    }
  ]
}

One important detail: hooks run asynchronously by default. Async means the hook fires and runs in the background without pausing Claude Code to wait for it. The session keeps moving. This is intentional. You don’t want a slow hook blocking the whole conversation. For logging and metadata tasks (like the example below), async is exactly what you want.


The session awareness example

Here’s the problem that led to building something real.

When you run multiple Claude Code sessions at once (which is common when you’re parallelizing work), they all show up in your terminal multiplexer as sessions with machine-generated names. Coming back to 13 of them feels like walking into an office where every desk looks identical. You can’t tell who’s doing what.

The first attempt was a script called whodis that scanned tmux pane scrollback, looking for the prompt character to reconstruct what was happening. It sort of worked. It was also fragile and annoying.

The better solution was already built into Claude Code. There’s a feature called the statusline, a one-line display that shows current session information. It was just turned off.

But more useful than the statusline alone is generating a topic for each session: a short human-readable description of what this Claude session is actually working on.

Here’s where hooks come in.

Generating topics with Haiku

The solution uses a PostToolUse hook to call Claude Haiku (Anthropic’s fast, cheap model, built for exactly this kind of lightweight task) after every 20 tool uses. Haiku reads the recent conversation history and returns a short topic description like “Terminal session naming with tmux” or “Update Britt’s LinkedIn profile.”

That topic gets written to the statusline. Now every terminal window shows a human-readable name for what’s happening inside it.

A few things worth knowing from building this:

Haiku is the right model here. The full Sonnet or Opus models are overkill for “summarize this conversation in a few words.” Haiku is fast, costs almost nothing, and is accurate enough for this task. If you’re calling Claude from inside Claude Code, Haiku is usually the right choice for background work.

The --system-prompt flag matters. The first attempt didn’t include it. Haiku read the CLAUDE.md instructions (which include “start every session with the current date”), followed them, and returned “It’s Tuesday, March 3, 2026.” That’s technically correct behavior, just not useful here. Adding --system-prompt to override the default with “return only a short topic description” fixed it.

Topics evolve. Because the hook fires on a message interval (every 20 messages), the topic updates as the session continues. A session that starts as “Debug the auth flow” might update to “Ship cohort email” after the work shifts. The name follows the work.

You never see it run. That’s the whole point of async hooks. Haiku is generating metadata in the background. Claude Code is answering your next question. The topic just appears, updated, like it was always there.

The auto-logger

The same hook also writes an entry to a log file every time it fires. Not a separate log, but one that appends to an existing log.jsonl (newline-delimited JSON, where each line is one JSON object) in Mission Control, the task management system already running.

This means every session produces a work diary automatically. What was touched, when, in what order. You don’t have to write it yourself and you don’t have to remember to run a script at the end.

The whole system is async hooks calling Haiku to name what Claude is doing. Claude calling Claude to describe itself. Recursive and slightly unhinged, but it works.


Other things you can do with hooks

Once you understand the pattern, hooks become a general tool for attaching behavior to any Claude Code event.

Security gates. A PreToolUse hook on bash can scan the command before it runs and block anything that looks dangerous. Check for rm -rf, unexpected sudo calls, or writes to sensitive directories. Exit with a non-zero code to stop the tool from running.

Auto-formatting. A PostToolUse hook on write_file can run your formatter automatically after every file write. No more “I forgot to run prettier.” The hook does it for you.

Deployment logging. Hook into bash tool use, detect when a deploy command runs, and write a timestamped entry to a deployment log. Now you have a record of every deploy without having to maintain it yourself.

Desktop notifications. Long-running sessions sometimes finish while you’re focused on something else. A Stop hook can send a system notification when the session ends so you know it’s done.

Audit trails for sensitive work. If Claude Code has access to production systems, a PostToolUse hook that logs every tool call to a structured file gives you a complete audit trail. Useful for compliance, debugging, or just knowing what happened when something breaks.

The pattern is the same in every case: pick an event, write a script, register it. The work happens automatically from then on.


A note on hook input

When a hook fires, Claude Code passes a JSON object to your script’s stdin. The shape depends on which event fired:

For PreToolUse and PostToolUse, the object includes the tool name, the inputs passed to the tool, and (for PostToolUse) the result.

For Notification, it includes the notification message.

For Stop, it includes basic session info.

Your script reads this with something like input=$(cat) in bash, or import json, sys; data = json.load(sys.stdin) in Python. From there, you can extract whatever you need and act on it.


Further reading

Common Questions

What are Claude Code hooks?

Hooks are scripts that run automatically when a Claude Code event fires. You register a shell script in ~/.claude/settings.json, tied to events like PreToolUse or PostToolUse. The script receives JSON context via stdin and runs asynchronously in the background.

What events can Claude Code hooks listen to?

Four events: PreToolUse (before a tool runs, can block it), PostToolUse (after a tool finishes), Notification (on status messages), and Stop (when the session ends). Each event passes relevant JSON context to the hook script.

How do I create a Claude Code hook?

Add a hook definition to ~/.claude/settings.json under the hooks key. Specify the event name, a matcher (tool name or "*" for all), and the path to your script. The script reads JSON from stdin and exits. Non-zero exit codes signal errors.

Can Claude Code hooks block dangerous commands?

Yes. A PreToolUse hook on the bash tool can scan the command before execution and exit with a non-zero code to block it. This is useful for preventing rm -rf, unauthorized sudo calls, or writes to sensitive directories.


A note from Alex: hi i’m alex - i run code for creatives. i’m a writer so i feel that it is important to say - i had claude write this piece based on my ideas and ramblings, voice notes, and teachings. the concepts were mine but the words themselves aren’t. i want to say that because its important for me to distinguish, as a writer, what is written ‘by me’ and what’s not. maybe that idea will seem insane and antiquated in a year, i’m not sure, but for now it helps me feel okay about putting stuff out there like this that a) i know is helpful and b) is not MY voice but exists within the umbrella of my business and work. If you have any thoughts or musings on this, i’d genuinely love to hear them - its an open question, all of this stuff, and my guess is as good as yours.

Ready to build this yourself?

Join the next cohort of Code for Creatives

Join the Next Cohort →