# Claude Code hooks: how to set up your first hook, step by step

> Claude Code hooks are shell commands that Claude Code runs automatically at set points in a session, such as before a tool call or when Claude needs your input. You add a hooks block to a settings file like `~/.claude/settings.json`, check it with /hooks, then trigger the event. A hook always runs, which is why it suits rules that cannot have exceptions.

Mindket editors · Published 2026-11-04 · Updated 2026-11-05 · https://mindket.com/learn/claude-code/claude-code-hooks

## In short

- A hook is a shell command tied to an event, so certain actions always happen instead of depending on the model to choose them.
- You write hooks as JSON in a settings file: an event, a matcher that filters it, and the handler that runs. `/hooks` shows what loaded.
- Exit 2 from a `PreToolUse` hook blocks the tool call and sends your stderr message to Claude. Exit 0 means no objection.
- Command hooks run with your full user permissions, so read every command before you add it.

## Before you start

You need Claude Code from [lesson 1](https://mindket.com/learn/claude-code/install-claude-code) and a settings file you can edit. The first hook sends a desktop notification, so it suits a machine with a desktop. The second hook is a Bash script that calls `jq`, which [Anthropic's hooks guide](https://code.claude.com/docs/en/hooks-guide) installs with `brew install jq` on macOS or `apt-get install jq` on Debian and Ubuntu.

One rule applies to every hook you write. Anthropic's reference warns about what they can reach.

> Command hooks execute shell commands with your full user permissions. They can modify, delete, or access any files your user account can access. Review and test all hook commands before adding them to your configuration.
>
> Anthropic, Claude Code documentation, [Claude Code docs: Hooks reference](https://code.claude.com/docs/en/hooks) (read 2026-11-04)

## Set up your first hook: a notification when Claude needs you

Anthropic's own walkthrough starts with a notification, so you do not have to watch the terminal while Claude waits. The block below is the macOS version. It uses the `Notification` event, which fires when Claude is waiting for your input or permission.

````json
{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}
````

1. Open `~/.claude/settings.json`, creating it if it does not exist, and add the block above. On Linux, use `notify-send 'Claude Code' 'Claude Code needs your attention'` as the `command`. On Windows, copy the PowerShell command from the Windows tab of [the hooks guide](https://code.claude.com/docs/en/hooks-guide).
2. If the file already has a `hooks` key, add `Notification` next to the event names that are there. Each event is a key inside the one `hooks` object, so do not replace the whole object.
3. Save, then type `/hooks` at the Claude Code prompt. Your hook shows up in the list under `Notification`.
4. Press `Esc` to go back to the prompt. Press `Shift+Tab` until the status bar shows `⏸ manual mode on`, ask Claude to do something that needs permission, then switch to another window. A desktop notification should arrive. Manual mode matters here because in auto mode a classifier reviews actions instead of asking you (lesson 2).

## Events, matchers and where a hook lives

A hook has three layers, as [the reference](https://code.claude.com/docs/en/hooks) describes. First an event, like `PreToolUse` or `Stop`. Then a matcher group that filters when it fires, like "only for the Bash tool". Then the handler, which is the shell command. Your first hook used the empty matcher `""`, which fires on every notification type. Setting it to `permission_prompt` would fire it only when Claude needs approval, after the prompt has waited about six seconds.

For tool events the matcher is a tool name. `Bash` matches the Bash tool, and `Edit|Write` matches either of two tools. The guide says matchers are case-sensitive and must match the tool name exactly.

Where you save the block decides who gets the hook.

**Hooks by the numbers in Anthropic's reference**

- **33** hook events, from `SessionStart` to `SessionEnd` (Source: [Claude Code docs: Hooks reference](https://code.claude.com/docs/en/hooks), read 2026-11-05)
- **5** handler types: `command`, `http`, `mcp_tool`, `prompt` and `agent` (Source: [Claude Code docs: Hooks reference](https://code.claude.com/docs/en/hooks), read 2026-11-05)
- **600 s** is the default timeout for `command`, `http` and `mcp_tool` hooks on most events (Source: [Claude Code docs: Hooks reference](https://code.claude.com/docs/en/hooks), read 2026-11-05)
- **1.5 s** is all a `SessionEnd` hook gets by default when you exit or run `/clear` (Source: [Claude Code docs: Hooks reference](https://code.claude.com/docs/en/hooks), read 2026-11-05)

- `~/.claude/settings.json`: all your projects, and not shared with anyone.
- `.claude/settings.json`: one project, and it can be committed so the team gets it too.
- `.claude/settings.local.json`: one project, and not committed.
- A plugin's `hooks/hooks.json`, or a skill's frontmatter: the hook travels with that plugin or skill.

| Event | When it fires | What the matcher filters |
| --- | --- | --- |
| `SessionStart` | When a session begins or resumes | How it started, such as `startup` or `resume` |
| `UserPromptSubmit` | When a prompt is submitted, before Claude processes it | Nothing, it always fires |
| `PreToolUse` | Before a tool call executes, and it can block the call | The tool name |
| `PostToolUse` | After a tool call succeeds | The tool name |
| `Notification` | When Claude Code sends a notification | The notification type |
| `Stop` | When Claude finishes responding | Nothing, it always fires |
| `PreCompact` | Before context compaction | `manual` or `auto` |
| `SessionEnd` | When a session terminates | Why it ended |

## A hook that says no: protect files from edits

A notification only reports. A `PreToolUse` hook can stop the action. This script, from the guide, reads the file path Claude is about to edit and refuses `.env`, `package-lock.json` and anything under `.git/`. The message it prints to stderr goes back to Claude, so Claude can adjust.

It parses JSON with `jq` and exits 2 to block. Save it as `.claude/hooks/protect-files.sh`.

````bash
#!/bin/bash
# protect-files.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Normalize Windows backslash separators so the patterns below match
FILE_PATH="${FILE_PATH//\\//}"

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0
````

1. Make the script executable on macOS and Linux with `chmod +x .claude/hooks/protect-files.sh`. Claude Code cannot run a hook script that is not executable.
2. Register it in `.claude/settings.json` as a `PreToolUse` hook with the matcher `Edit|Write`. The `command` is `"$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files.sh`, written with escaped quotes inside the JSON string.
3. Ask Claude to add a comment to your `.env` file. Claude Code blocks the edit before it runs and passes the `Blocked:` message to Claude.

## How a hook answers: exit codes and input

Claude Code sends the event as JSON on the hook's standard input. Every event carries fields like `session_id` and `cwd`, and each type adds its own. A `PreToolUse` hook for Bash sees the tool name and the command, as in the example below. That is why the script above reads `.tool_input.file_path` for an edit.

The hook answers with its exit code. Exit 0 from a `PreToolUse` hook is not an approval, because the tool call still goes through the normal permission flow. For a hook that must decide with more detail, Claude Code also reads a JSON object from stdout when you exit 0. Pick one approach per hook, exit 2 or JSON, and do not mix them.

````json
{
  "session_id": "abc123",
  "cwd": "/Users/sarah/myproject",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}
````

| Exit code | What Claude Code does | Where your message goes |
| --- | --- | --- |
| 0 | Success, and the action goes ahead | Plain-text stdout reaches Claude only on `UserPromptSubmit`, `UserPromptExpansion`, `SessionStart` and `PostModelSwitch` |
| 2 | Blocking error, so on events that can block, the action stops | Your stderr becomes the reason, and Claude gets it as feedback on most events |
| Anything else | Non-blocking error, so the action goes ahead | A hook error notice shows the first line of stderr after "Failed with non-blocking status code:" |

## How Claude Code's creator uses a format hook and notifications

Boris Cherny created Claude Code at Anthropic. His January 2026 setup thread [on X](https://x.com/bcherny/status/2007179852047335529) says his team formats Claude's code with a `PostToolUse` hook. Claude usually writes well-formatted code, and the hook fixes the rest before a CI check fails on it. The auto-format hook in "Try it yourself" below is the same idea: `PostToolUse`, the matcher `Edit|Write` and Prettier.

[The first tip in that thread](https://x.com/bcherny/status/2007179833990885678) is about waiting. He runs five Claude sessions at once in numbered terminal tabs and relies on system notifications to learn when one needs input. Your first hook in this lesson gives you that signal in any terminal, and the `permission_prompt` matcher narrows it to approval requests.

> Claude usually generates well-formatted code out of the box, and the hook handles the last 10% to avoid formatting errors in CI later.
>
> Boris Cherny, Creator of Claude Code, Anthropic, [X](https://x.com/bcherny/status/2007179852047335529) (read 2026-11-05)

[![Boris Cherny (@bcherny) on X, 2 January 2026, tip 9 of his setup thread. We use a PostToolUse hook to format Claude's code. Claude usually generates well-formatted code out of the box, and the hook handles the last 10% to avoid formatting errors in CI later.](https://mindket.com/blog/shots/x-bcherny-posttooluse-hook.webp?v=9841d5ab371b)](https://x.com/bcherny/status/2007179852047335529)

Boris Cherny on X, 2 January 2026

[![Boris Cherny (@bcherny) on X, 2 January 2026, tip 1 of his setup thread. I run 5 Claudes in parallel in my terminal. I number my tabs 1-5, and use system notifications to know when a Claude needs input.](https://mindket.com/blog/shots/x-bcherny-five-claudes.webp?v=d9904b875672)](https://x.com/bcherny/status/2007179833990885678)

Boris Cherny on X, 2 January 2026

> Screenshots of X's own embeds of the posts, taken on 5 November 2026, with the time in UTC. Select a screenshot to open the post on X.

## Common mistakes

- A matcher that does not match. Matchers are case-sensitive and compare against the tool name, not the words in your prompt. Check the spelling, then check `/hooks` to see the hook under the event you meant.
- Replacing the `hooks` object instead of adding to it. Each event name is a sibling key inside the one `hooks` object.
- Invalid JSON in the settings file. Trailing commas and comments are not allowed, and a broken file leaves `/hooks` with nothing configured. Edits are normally picked up on their own. If a few seconds pass and nothing appears, restart the session.
- A script that is not executable, or one that cannot be found. Run `chmod +x` on it and point to it with `$CLAUDE_PROJECT_DIR`. If the transcript says `jq: command not found`, install `jq`.
- Expecting a successful hook to say something. It shows nothing, so check its effect, like a reformatted file, or press `Ctrl+O` to open the transcript view.
- Expecting `PostToolUse` to undo anything. The tool has already run. A formatter hook with the matcher `Edit|Write` also misses files that a Bash command rewrites.

## Try it yourself

- Change the notification hook's matcher to `permission_prompt` and repeat the test. Then set it back to `""` and compare when each one fires.
- Test the protect script by hand before you trust it: `echo '{"tool_input":{"file_path":".env"}}' | .claude/hooks/protect-files.sh`, then `echo $?`. You should see the Blocked line and a 2.
- Open `/hooks` and select All events at the end of the list, to browse every event, including the ones with no hook.
- Add the guide's auto-format hook to `.claude/settings.json`. It is a `PostToolUse` hook with the matcher `Edit|Write` that runs Prettier on the edited file, and it needs `jq` and Prettier. Ask Claude to add single-quoted strings to a JavaScript file, then open the file.

## Next: Agent Skills

That is the Claude Code course: install, plan mode, slash commands and hooks. The next course, [Agent Skills](https://mindket.com/learn/agent-skills), goes deeper on the SKILL.md format behind the commands you wrote in lesson 3.

Hooks also travel inside plugins, which our [plugins guide](https://mindket.com/blog/claude-code-plugins) covers. A project's standing instructions still belong in a [CLAUDE.md file](https://mindket.com/blog/claude-md-file), because a hook enforces and a CLAUDE.md file explains.

## Check yourself

1. Your `PreToolUse` hook script must block an edit. Which exit code does it use?

   A. Exit 0
   B. Exit 1
   C. Exit 2

   Answer: C. Exit 2 blocks the tool call and sends your stderr message to Claude, while exit 1 is a non-blocking error and the action goes ahead.

2. Where do you save a hook so it applies to one project and your team gets it when you commit?

   A. `.claude/settings.json`
   B. `~/.claude/settings.json`
   C. `.claude/settings.local.json`
   D. A plugin's `hooks/hooks.json`

   Answer: A. That file belongs to one project and can be committed, while `~/.claude/settings.json` covers all your projects and is not shared.

3. Which matcher makes a `PreToolUse` hook fire for either the Edit tool or the Write tool?

   A. `edit|write`
   B. `Edit Write`
   C. `Edit&Write`
   D. `Edit|Write`

   Answer: D. `Edit|Write` matches either tool, and matchers are case-sensitive and must match the tool name exactly.

4. Your settings file already has a `hooks` key and you want to add a second event. What do you do?

   A. Replace the whole `hooks` object with the new one
   B. Add the new event name beside the others inside the one `hooks` object
   C. Write a second `hooks` key at the top of the file

   Answer: B. Each event is a key inside the one `hooks` object, so you add to it and never replace it.

## Frequently asked questions

### What are Claude Code hooks used for?

They run shell commands at points in a session, for example to format files after edits, block commands before they run, send a notification when Claude needs input, or add context at session start. Anthropic describes the gain as deterministic control: certain actions always happen, rather than relying on the model to choose to run them.

### How do I see which hooks are active?

Type `/hooks`. It opens a read-only browser of your configured hooks, each labeled with where it comes from, such as user settings, project settings or a plugin. Select a hook to see the full command and the file that defines it.

### Can a hook stop Claude from doing something?

Yes. A `PreToolUse` hook that exits 2, or returns a deny decision, cancels the tool call. That block holds in every permission mode, including `bypassPermissions`, because `PreToolUse` hooks fire before the permission-mode check.

### How do I turn hooks off?

Set `"disableAllHooks": true` in your settings file, or pass `--settings '{"disableAllHooks": true}'` to turn them off for one run. You cannot disable one hook and keep it in the configuration. Hooks set in managed settings keep running unless that setting is also on at the managed level.

### Are hooks from the internet safe to copy?

Only after you have read them. Whatever the command does, it does as you, on any file your account can reach. In an interactive session, Claude Code also holds back hooks from every settings file until you accept the workspace trust dialog for the folder.

### Do I need to restart Claude Code after editing a hook?

Usually not. When you edit a settings file while Claude Code runs, the file watcher normally picks up hook changes on its own. If `/hooks` still does not show your change after a few seconds, restart the session to force a reload.

### Can a skill or a plugin carry its own hooks?

Yes. A plugin ships hooks in `hooks/hooks.json`, and a skill can declare them in its frontmatter, where they run for the rest of the session once the skill is invoked. A subagent's frontmatter can too, and those hooks run while that subagent is running.

## Sources

- [Claude Code docs: Automate actions with hooks](https://code.claude.com/docs/en/hooks-guide) (read 2026-11-04)
- [Claude Code docs: Hooks reference](https://code.claude.com/docs/en/hooks) (read 2026-11-05)
- [Boris Cherny on X: tip 9, a PostToolUse format hook, 2 January 2026](https://x.com/bcherny/status/2007179852047335529) (read 2026-11-05)
- [Boris Cherny on X: tip 1, five sessions and notifications, 2 January 2026](https://x.com/bcherny/status/2007179833990885678) (read 2026-11-05)

[Open the Agent Skills course](https://mindket.com/learn/agent-skills)
