# Claude Code hooks: a practical guide with examples

URL: https://agentvera.dev/en/blog/claude-code-hooks-guide/ · Published: 2026-10-23

How Claude Code hooks work: events, settings.json, matchers, exit codes and JSON output, plus ready hooks that format files, block risky commands and notify you.

**In short:**

- Hooks are defined under the hooks key in settings.json and always run at their event, because Claude Code runs them, not the model.
- To block a tool call, a PreToolUse hook exits with code 2 or returns JSON with permissionDecision set to deny; exit code 1 does not block anything.
- A deny hook still blocks calls under --dangerously-skip-permissions, but hooks are not a sandbox and run with your user's permissions.

Claude Code hooks are commands that Claude Code runs on its own at fixed points in a session: before a tool call, after a file edit, when a prompt is sent, when Claude stops or needs your attention. You define them under the `hooks` key in `settings.json`. A hook can add context, format a file, or block an action by exiting with code 2 or returning a JSON decision. Unlike instructions in `CLAUDE.md`, a hook always runs, because the harness runs it, not the model.

This guide covers the events that matter, the config format, matchers, exit codes and JSON output, three hooks you can copy today, and the security notes to read before you add any. The facts come from the official [hooks reference](https://code.claude.com/docs/en/hooks "Claude Code hooks reference") and [hooks guide](https://code.claude.com/docs/en/hooks-guide "Claude Code hooks guide"). If you only want the short definition, see the [Claude Code hooks glossary entry](/en/glossary/claude-code-hooks/ "Claude Code hooks").

## Why use hooks instead of instructions

You can write “always run Prettier after editing” in `CLAUDE.md`, and Claude will usually do it. Usually is the problem. Instructions are advice the model weighs against everything else in its context. Hooks are code that runs every time the event fires.

That makes hooks the right tool for three kinds of rules:

- **Things that must always happen**, like formatting, linting or logging.
- **Things that must never happen**, like editing `.env` or running `rm -rf` on the wrong folder.
- **Things you want to know about**, like Claude waiting for a permission answer while you are in another window.

Keep style advice and project knowledge in [CLAUDE.md](/en/blog/claude-md-agents-md/ "CLAUDE.md and AGENTS.md") and put hard rules in hooks.

## Hook events you will actually use

The reference lists more than thirty events, and the list grows with new releases. Most real setups use a handful:

| Event | When it fires | Can it block? |
| --- | --- | --- |
| `PreToolUse` | Before a tool call runs | Yes, blocks the call |
| `PostToolUse` | After a tool call succeeds | No, the tool already ran; stderr goes to Claude |
| `UserPromptSubmit` | When you send a prompt | Yes, the prompt never reaches Claude |
| `Notification` | When Claude Code sends a notification, such as a permission prompt or idle wait | No |
| `Stop` | When Claude finishes its answer | Yes, Claude keeps working |
| `SubagentStart` / `SubagentStop` | When a subagent starts or finishes | Only `SubagentStop` |
| `SessionStart` | On startup, resume, `/clear` or compaction | No, but stdout becomes context |
| `PreCompact` | Before the context is compacted | Yes, blocks compaction |
| `PermissionRequest` | When a permission dialog is about to show | Through JSON only, not exit 2 |
| `SessionEnd` | When the session ends | No |

`PreToolUse` and `PostToolUse` cover most needs. `Notification` and `Stop` are the ones for “tell me when it is done or stuck”.

## How hooks are configured in settings.json

Hooks live in the same settings files as permissions:

| File | Scope | Shared through git? |
| --- | --- | --- |
| `~/.claude/settings.json` | All your projects | No |
| `.claude/settings.json` | One project | Yes |
| `.claude/settings.local.json` | One project, only you | No (gitignored) |
| Managed policy settings | Whole organization | Set by admins |

Plugins can ship hooks in `hooks/hooks.json`, and skills and subagents can define hooks in their frontmatter.

The structure has three levels: the event, a matcher group, and a list of handlers.

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

A handler’s `type` can be `command` (a shell command), `http` (a POST to a URL), `mcp_tool` (a tool on an MCP server you have connected), `prompt` (a quick yes/no check by a model) or `agent` (an experimental subagent check). The default timeout is 600 seconds for command, HTTP and MCP tool handlers, 30 seconds for prompt hooks and 60 for agent hooks.

Every hook gets JSON on stdin. Common fields are `session_id`, `transcript_path`, `cwd`, `permission_mode` and `hook_event_name`. Tool events add `tool_name`, `tool_input` and `tool_use_id`. Your script reads that JSON, usually with `jq`, and decides what to do. The environment variable `CLAUDE_PROJECT_DIR` points to the project root, which is handy for referencing scripts stored in the repo.

You can type `/hooks` inside Claude Code to browse what is loaded and which file each hook came from. The browser is read only, so you change hooks by editing the JSON.

## Matchers: which tool calls a hook sees

The `matcher` decides which calls trigger a hook. For tool events it matches the tool name:

| Matcher | Meaning |
| --- | --- |
| `"*"`, `""` or no matcher | Every call |
| `"Bash"` | Exactly the Bash tool |
| `"Edit\|Write"` | Edit or Write (a list) |
| `"mcp__memory__.*"` | Every tool from the `memory` MCP server |
| `"mcp__.*__write.*"` | Any MCP tool whose name starts with `write` |

The rule is simple. If the matcher has only letters, digits, `_`, `-`, spaces, commas or `|`, it is an exact name or a list of names. Any other character turns it into a JavaScript regular expression. MCP tools are named `mcp__<server>__<tool>`.

Some events ignore the matcher, including `UserPromptSubmit` and `Stop`, because there is nothing to match. Others match something other than a tool name. `SessionStart` matches how the session started (`startup`, `resume`, `clear`, `compact`), and `Notification` matches the notification type, such as `permission_prompt` or `idle_prompt`.

Tool events also accept an `if` field with permission rule syntax, so a Bash hook can run only for, say, `"Bash(git *)"`. The docs describe this as a best-effort filter, so keep the real check in your script.

## Exit codes and JSON output

A command hook talks back to Claude Code in two ways: the exit code, and optional JSON on stdout.

| Exit code | What happens |
| --- | --- |
| `0` | Success. stdout is parsed as JSON if it is a single JSON object. For `UserPromptSubmit` and `SessionStart`, plain text on stdout is added to Claude’s context. |
| `2` | Blocking error. stderr is the reason. On `PreToolUse` the call is cancelled, on `UserPromptSubmit` the prompt is rejected, on `Stop` Claude keeps going. |
| Anything else, including `1` | Non-blocking error. Claude Code notes it and carries on. |

The trap here is exit code 1. Many scripts fail with 1 by default, and that does **not** block anything. If you mean “stop”, exit 2.

For finer control, exit 0 and print JSON. A `PreToolUse` hook can allow, deny or ask:

```json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Use rg instead of grep for better performance"
  }
}
```

`permissionDecision` takes `allow`, `deny`, `ask` or `defer`. A hook can also rewrite the call with `updatedInput`, or pass extra text to Claude with `additionalContext`. Fields that work on any event include `continue: false` (stop Claude entirely, with a `stopReason`) and `systemMessage` (a warning shown to you).

When several hooks match, they run in parallel, and the most restrictive answer wins: deny beats defer, defer beats ask, ask beats allow. An `allow` from a hook does not override a deny rule in your permissions.

## Example 1: format files after every edit

This is the hook most people start with. It runs Prettier on any file Claude writes or edits:

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

Swap in `ruff format`, `gofmt -w` or `rustfmt` for other languages. Since the formatter runs after the edit, Claude sees the formatted file next time it reads it, and you stop seeing style-only diffs in review.

## Example 2: block dangerous commands and protected files

A `PreToolUse` hook is the place for hard limits. This script, adapted from the official guide, blocks edits to sensitive files. Save it as `.claude/hooks/protect-files.sh`:

```bash
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

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
```

Make it executable with `chmod +x .claude/hooks/protect-files.sh`, then register it:

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}
```

The same pattern works for shell commands. Match `Bash`, read `.tool_input.command`, and exit 2 when it contains something you never want, such as `rm -rf /`, `git push --force` or `DROP TABLE`. Keep in mind that a string check is easy to get around, so treat it as a seatbelt, not a lock.

## Example 3: get a desktop notification

Long tasks stop and wait for you more often than you’d like. A `Notification` hook tells you right away. On macOS:

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

On Linux, use `notify-send 'Claude Code' 'Claude Code needs your attention'` instead. Set the matcher to `permission_prompt` if you only care about permission requests, or add a `Stop` hook with the same command to hear when Claude finishes.

## More useful patterns

- **Re-inject context after compaction.** A `SessionStart` hook with matcher `compact` can `echo` a short reminder of your rules. Its stdout is added to the fresh context.
- **Audit config changes.** A `ConfigChange` hook can append each settings change to a log file.
- **Quality gate on Stop.** A `Stop` hook can run your tests and exit 2 with the failures, so Claude keeps fixing. Check the `stop_hook_active` field in the input to avoid an endless loop. Claude Code also overrides a Stop hook after several blocks in a row.
- **Auto-approve one safe prompt.** A `PermissionRequest` hook can answer a specific dialog with `{"behavior": "allow"}` inside its decision object. Keep the matcher narrow.

## Security notes before you add hooks

Hooks run as shell commands with your user’s permissions, without asking. That is the whole point, and also the risk.

- **Read hooks before you trust a repo.** A project’s `.claude/settings.json` can ship hooks. Review them the way you would review a `postinstall` script.
- **Quote your variables.** Hook input comes from the model, and the model reads files and web pages that may contain [prompt injection](/en/glossary/prompt-injection/ "Prompt injection"). Use `"$VAR"`, never eval input, and prefer `jq` over hand-rolled parsing.
- **Use absolute or project-relative paths** with `$CLAUDE_PROJECT_DIR`, so a hook does not depend on the current folder.
- **Keep hooks fast.** A slow `PreToolUse` hook slows every tool call. Set a `timeout`.
- **Hooks are not a sandbox.** A deny hook blocks calls even under `--dangerously-skip-permissions`, which is useful, but a determined or confused agent can reach the same result another way. Combine hooks with deny rules and isolation, as covered in [AI coding agent security](/en/blog/ai-coding-agent-security/ "AI coding agent security") and [skipping permissions safely](/en/blog/claude-code-skip-permissions/ "Claude Code --dangerously-skip-permissions").
- **Know the off switches.** `"disableAllHooks": true` turns hooks off, and admins can set `allowManagedHooksOnly` in managed settings to block user and project hooks.

When a hook misbehaves, start Claude Code with `claude --debug`, or write the log to a file with `--debug-file /tmp/claude.log`, to see each hook’s matcher, exit code and output.

## How AgentVera uses hooks

AgentVera is a desktop app that runs Claude Code, Codex, Gemini CLI and other coding CLIs side by side. Hooks are a big part of how it works with Claude Code.

- **Your hooks stay yours.** AgentVera passes its own hooks to each Claude Code process with the `--settings` flag. Claude Code merges them with the hooks in your settings files, and your `settings.json` is not edited.
- **Agent status comes from hooks.** The status each agent shows in the app is read from the CLI’s own hook events, not guessed from the terminal text.
- **Smart tools are hooks.** The [smart tools](/en/features/smart-tools/ "Smart tools") feature adds four hooks to local Claude Code agents: it blocks repeated reads of the same file, runs only the tests an edit affects, checks for errors right after an edit, and shares results between agents. They are on by default and you can turn them off under Settings → “Token saving”.
- **No notify script needed.** [Notifications and approvals](/en/features/notifications-and-approvals/ "Notifications and approvals") gather every agent waiting for approval in one queue, send desktop notifications while the app is in the background, and can ping your phone through Telegram.

Qwen Code gets AgentVera’s hooks as well, because its hook system follows Claude Code’s events and JSON format.

## Wrapping up

Start with one `PostToolUse` formatter and one `PreToolUse` guard, check them with `/hooks` and `--debug`, and add more only when a rule keeps getting broken. Exit 2 to block, print JSON when you need more control, and treat every hook as code that runs with your permissions.

Read more:

- [Claude Code hooks (glossary)](/en/glossary/claude-code-hooks/ "Claude Code hooks")
- [Smart tools](/en/features/smart-tools/ "Smart tools")
- [Notifications and approvals](/en/features/notifications-and-approvals/ "Notifications and approvals")
- [Claude Code agent](/en/agents/claude-code/ "Claude Code in AgentVera")

To try AgentVera, start at the [home page](/en/ "AgentVera").

## Frequently asked questions

**What are Claude Code hooks?** Hooks are shell commands, HTTP calls, MCP tools or prompts that Claude Code runs on its own at set points in a session, such as before a tool call, after a file edit, or when Claude needs your attention. You define them in settings.json.

**Where do I put Claude Code hooks?** In the hooks key of a settings file: ~/.claude/settings.json for all your projects, .claude/settings.json for a project you share through git, or .claude/settings.local.json for a project only on your machine. Plugins, skills and subagents can define hooks too.

**How does a hook block a tool call?** A PreToolUse hook either exits with code 2 and writes the reason to stderr, or exits 0 and prints JSON with permissionDecision set to deny. Claude Code cancels the call and shows the reason to Claude. Exit code 1 does not block.

**Do hooks still run with --dangerously-skip-permissions?** Yes. A hook that returns deny blocks the tool call even in bypassPermissions mode. That makes a PreToolUse hook a useful last guard for commands you never want, though it is not a sandbox.

**How do I debug a Claude Code hook that does not fire?** Open /hooks to see which hooks are loaded and where they come from, check that the matcher matches the exact tool name, and start Claude Code with --debug or --debug-file to see each hook run, its exit code and its output.

---

**About AgentVera.** AgentVera is a desktop app for macOS, Windows and Linux, built by Solviera Teknoloji. It runs 18 AI coding agents (Claude Code, Codex, Gemini CLI, Copilot, Cursor, Antigravity, opencode, Hermes, Qwen Code, Kimi Code, Kiro, Amp, Droid, Goose, Vibe, Crush, Aider, Cline) side by side, per project, in real terminals with the user's own CLI installs and logins. The free plan has unlimited agents. Download: https://agentvera.dev/en/download/ · Pricing: https://agentvera.dev/en/pricing/
