# How to use hooks in Claude Code: setup, examples and fixes

> Hooks run your shell commands at set points in Claude Code's lifecycle. Here is how to add one, block edits to protected files, and fix a hook that never fires.

URL: https://claudegpt.si/news/claude-code-how-to-use-hooks  
Published: 2026-10-31T10:52:21.978Z  
Author: ClaudeGPT Si Newsroom  
Section: Guides  
Topics: Claude Code, Anthropic, Hooks, How to, Settings  
Publisher: ClaudeGPT Si

## Key takeaways

- A hook is a hooks block in a settings file that runs a command when an event such as PreToolUse, PostToolUse or Notification fires.
- Exit code 2 blocks an action and sends stderr to Claude as feedback, while exit 1 and other codes do not block.
- A PreToolUse hook that denies a call blocks it in every permission mode, including bypassPermissions.

Claude Code hooks are shell commands that Claude Code runs at fixed points in its lifecycle, so an action happens every time instead of only when the model remembers to do it. To use hooks in Claude Code, you add a `hooks` block to a settings file, pick an event such as `PostToolUse`, and point it at a command. This guide follows [Anthropic's hooks guide](https://code.claude.com/docs/en/hooks-guide) and the [hooks reference](https://code.claude.com/docs/en/hooks), and shows how to set up your first hook, block edits to protected files and fix a hook that does nothing.

If you only want fewer permission prompts, hooks are one route and allow rules are another. Our guide to [allowing all commands in Claude Code](https://claudegpt.si/news/claude-code-allow-all-commands) covers rules and permission modes, and the section on permissions below explains how hooks fit with them.

## What you need first

You need Claude Code installed and a settings file you can edit. Many of the docs' examples use `jq` to read JSON, so install it with `brew install jq` on macOS or `apt-get install jq` on Debian and Ubuntu. A hook that runs a script also needs that script to be executable.

## How a hook is configured

A hook has three levels: an event, a matcher group and one or more handlers. This example runs Prettier after Claude edits or writes a file:

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

When an event fires, Claude Code sends the event's data to your command as JSON on stdin. For a `PreToolUse` hook on a Bash call, that includes `hook_event_name`, `tool_name` and `tool_input`, where `tool_input.command` holds the shell command. Your script reads it, then tells Claude Code what to do through its exit code and output.

## How to set up your first hook

The docs' first walkthrough is a desktop notification for when Claude is waiting on you.

1. Open `~/.claude/settings.json`, creating it if needed, and add a `Notification` hook. On Linux the command is `notify-send 'Claude Code' 'Claude Code needs your attention'`. On macOS it is an `osascript` call, and on Windows a PowerShell command.

```json
{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
          }
        ]
      }
    ]
  }
}
```

2. If the file already has a `hooks` key, add `Notification` next to the existing event names. Each event is a key inside the single `hooks` object.
3. Type `/hooks` in Claude Code. The hooks browser lists the new hook under `Notification`.
4. Press `Shift+Tab` until the status bar shows `manual mode on`, ask Claude to do something that needs permission, and switch to another window. A notification should appear.

The empty matcher fires on every notification type. To limit it, set the matcher to a value such as `permission_prompt` or `idle_prompt`. According to the docs, `idle_prompt` fires when Claude finished about 60 seconds ago and you haven't typed since.

On Linux, `notify-send` needs a desktop notification daemon, which headless servers, SSH sessions and most containers lack. If the command is missing, the docs say to install the `libnotify-bin` package on Debian and Ubuntu.

## How to block edits to protected files

A `PreToolUse` hook runs before a tool call and can stop it. The docs use this to keep Claude away from `.env`, `package-lock.json` and `.git/`.

1. Save this to `.claude/hooks/protect-files.sh`:

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

2. Make it executable with `chmod +x .claude/hooks/protect-files.sh`.
3. Register it in `.claude/settings.json`:

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

4. Ask Claude to add a comment to your `.env` file. Claude Code blocks the edit and passes the `Blocked:` message back to Claude, so it can try another approach.

We ran the script's logic in a Linux shell with sample JSON. A `file_path` of `/app/.env` printed the Blocked message and exited with code 2, and `/app/index.js` exited with code 0. We did not run it inside a live Claude Code session.

## What exit codes and JSON do

Exit codes are how a hook answers.

| Result | What it does |
| --- | --- |
| Exit 0 | No objection. On `PreToolUse`, the normal permission flow still applies, because exit 0 does not approve the call. |
| Exit 2 | Blocks the action on events that can block. Stderr is the reason, and Claude receives it as feedback on most events. |
| Any other code | A non-blocking error. The action proceeds and a `hook error` notice appears. |

Exit 1 does not block. Use exit 2 for anything that must be enforced. For finer control, exit 0 and print a JSON object instead. This `PreToolUse` output denies a call and tells Claude why:

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

The docs say to pick one approach per hook, exit 2 or JSON, and not mix them. Fields like `permissionDecision` and `additionalContext` must sit inside `hookSpecificOutput`. Placed at the top level, they are ignored without an error.

## Which event to use

Anthropic lists more than 30 events. The ones most people need are `SessionStart`, `UserPromptSubmit`, `PreToolUse` (before a tool call, can block it), `PermissionRequest`, `PostToolUse` (after a call succeeds), `Stop` (Claude finishes responding), `Notification` and `ConfigChange`.

Matchers filter what a hook reacts to. For tool events the matcher is a tool name such as `Bash` or `Edit|Write`. A pattern like `mcp__github__.*` targets every tool from one MCP server. For `SessionStart` the matcher is how the session started: `startup`, `resume`, `clear`, `compact` or `fork`. Matchers are case-sensitive.

To narrow a tool hook by arguments too, add an `if` field using permission rule syntax, for example `"if": "Bash(git *)"`. It works only on tool events, and the docs call it best-effort, so use permission rules rather than a hook for a hard allow or deny.

## Where to put a hook

| Location | Scope | Shareable |
| --- | --- | --- |
| `~/.claude/settings.json` | All your projects | No |
| `.claude/settings.json` | One project | Yes, commit it |
| `.claude/settings.local.json` | One project | No, gitignored |
| Managed policy settings | Organization | Yes, admin-controlled |
| Plugin `hooks/hooks.json` | When the plugin is enabled | Yes |

Skill and subagent frontmatter can also hold hooks. When several hooks match one event, all of them run to completion. For `PreToolUse` decisions the most restrictive answer wins, in the order deny, defer, ask, allow.

## Other hook types

Most hooks use `"type": "command"`. Anthropic's guide describes four more: `http` posts the event JSON to a URL, `mcp_tool` calls a tool on an MCP server, `prompt` asks a Claude model for a single-turn yes or no decision, and `agent` starts a subagent that can read files and run commands to verify something. The docs mark agent hooks experimental.

## Hooks and permissions

A hook cannot loosen your rules. A hook that returns `allow` does not override deny rules from your settings. A hook that returns `deny` blocks the tool in every permission mode, including `bypassPermissions` and `--dangerously-skip-permissions`, because `PreToolUse` hooks run before the permission-mode check. That makes hooks useful as guardrails even when you run with prompts turned off.

To auto-approve one prompt, use a `PermissionRequest` hook with a narrow matcher. The docs' example approves only `ExitPlanMode`. They warn that an empty or `.*` matcher would approve every permission prompt, including file writes and shell commands.

## Fixes when a hook doesn't work

- **The hook never runs.** Run `/hooks` and check it sits under the right event. Check that the matcher matches the tool name exactly, and that you are using the right event, since `PreToolUse` fires before a tool and `PostToolUse` after.
- **`/hooks` shows nothing.** Check the JSON is valid, because trailing commas and comments are not allowed. Confirm the file is `.claude/settings.json` for a project or `~/.claude/settings.json` for global. Restart the session if the file watcher missed the change.
- **"hook error" in the transcript.** Test the script by hand with `echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh` and check `echo $?`. Use absolute paths or `${CLAUDE_PROJECT_DIR}` for "command not found", install `jq` if it is missing, and run `chmod +x` if the script isn't executable.
- **JSON output has no effect.** A line printed by your shell profile can reach stdout ahead of the JSON, so the output no longer starts with `{`. Wrap any `echo` in `~/.bashrc` or `~/.zshrc` so it runs only in interactive shells. Start Claude Code with `claude --debug` and search the log for `Hook output does not start with {`.
- **A Stop hook keeps Claude going.** Claude Code overrides a Stop hook after eight blocks in a row. Read `stop_hook_active` from the input and exit 0 when it is `true`.
- **Other limits.** `PostToolUse` hooks cannot undo a tool call that already ran. Command hooks time out after 10 minutes by default, prompt hooks after 30 seconds and agent hooks after 60.

To see what a hook did, press `Ctrl+O` for the transcript view. For full detail, start with `claude --debug-file /tmp/claude.log` and tail the file.

## Where to go next

Anthropic's [hooks reference](https://code.claude.com/docs/en/hooks) has every event's input schema, the output format and async hooks. Review its security considerations before deploying hooks in a shared or production setup.

## Frequently asked questions

### Where do I put Claude Code hooks?

Add a hooks block to ~/.claude/settings.json for all your projects, .claude/settings.json for one shared project, or .claude/settings.local.json for one project that stays out of git. Plugins and skill or subagent frontmatter can also hold hooks.

### How do I block a tool call with a Claude Code hook?

Have a PreToolUse hook exit with code 2 and write the reason to stderr, or exit 0 and print JSON with permissionDecision set to deny. Claude receives the reason as feedback.

### Why is my Claude Code hook not firing?

Run /hooks to confirm it is listed under the right event, check that the matcher matches the tool name exactly because matchers are case-sensitive, and make sure the JSON is valid and the script is executable.

### Do hooks work with --dangerously-skip-permissions?

Yes for denials. A PreToolUse hook that returns deny blocks the tool even in bypassPermissions mode, because PreToolUse hooks run before the permission-mode check.

## Sources

- [Automate actions with hooks](https://code.claude.com/docs/en/hooks-guide), Anthropic
- [Hooks reference](https://code.claude.com/docs/en/hooks), Anthropic
