# Claude Code Hooks: 7 Copy-Paste Examples That Actually Work

`CLAUDE.md` asks Claude Code to behave. A **hook** makes something happen every time, whether the model remembers or not. Hooks are shell commands (or HTTP calls, prompts or MCP tools) that Claude Code runs at fixed points: before a tool runs, after a file edit, when it needs your input, when a session starts, and so on.

This page gives seven hooks you can paste into a settings file today. They were checked against Anthropic's [hooks guide](https://code.claude.com/docs/en/hooks-guide) and [hooks reference](https://code.claude.com/docs/en/hooks) on 2026-10-17, and the shell script in example 4 was tested by piping sample tool input into it. Claude Code changes quickly, so re-check the docs for anything you rely on.

## Where hooks go

Hooks live under a `hooks` key in a settings file. Where you put them decides who they affect:

| File | Scope |
| --- | --- |
| `~/.claude/settings.json` | All your projects, only on your machine |
| `.claude/settings.json` | One project, shared with the team when you commit it |
| `.claude/settings.local.json` | One project, just you |

Each event name (`PreToolUse`, `PostToolUse`, `Notification`...) is a key inside the single `hooks` object. If your file already has hooks, add the new event next to the existing ones instead of replacing the whole object. Type `/hooks` in Claude Code to see what it loaded.

Most examples use [`jq`](https://jqlang.org/) to read the JSON that Claude Code sends to the hook on stdin. Install it first (`brew install jq` or `apt-get install jq`).

## The two rules that make hooks click

1. **Input arrives as JSON on stdin.** For a tool event you get `tool_name` and `tool_input` (for Bash, `tool_input.command`; for Edit and Write, `tool_input.file_path`).
2. **The exit code decides what happens.** Exit `0` means no objection. Exit `2` blocks the action, and whatever you print to stderr is shown to Claude as the reason, so it can change course. Other exit codes are reported as non-blocking errors and the action goes ahead.

## 1. Get a desktop notification when Claude needs you

Stop watching the terminal. Add to `~/.claude/settings.json` (Linux shown; on macOS use `osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'`):

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

An empty matcher fires on every notification type. Use `permission_prompt` to be told only when Claude waits for an approval, or `idle_prompt` for when it finished and you have not typed for about a minute.

## 2. Auto-format every file Claude edits

A `PostToolUse` hook with an `Edit|Write` matcher runs right after those tools succeed. Put this in `.claude/settings.json`:

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

Swap Prettier for `black`, `gofmt -w` or `rustfmt` as needed. A successful hook prints nothing, so check the file to confirm it ran.

## 3. Block edits to protected files

Save as `.claude/hooks/protect-files.sh` and run `chmod +x` on it:

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

for pattern in ".env" "package-lock.json" ".git/"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done
exit 0
```

Register it as a `PreToolUse` hook so it runs before the edit happens:

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

Note that Claude can also change files through shell commands, which an `Edit|Write` matcher does not see. For files that must never be read or touched, add a `deny` rule too (see our [settings.json permissions guide](https://agentkitco.com/blog/claude-code-settings-json-permissions/)).

## 4. Block force pushes, pushes to main, `reset --hard` and `rm -rf`

This is our own script. Save as `.claude/hooks/block-risky-bash.sh` and `chmod +x` it:

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

block() { echo "Blocked by hook: $1. Ask the user to run it themselves if it is really needed." >&2; exit 2; }

[[ "$CMD" =~ git[[:space:]]+push.*(--force|[[:space:]]-f([[:space:]]|$)) ]] && block "force push"
[[ "$CMD" =~ git[[:space:]]+push.*[[:space:]](main|master)([[:space:]]|$) ]] && block "direct push to main/master"
[[ "$CMD" =~ git[[:space:]]+reset[[:space:]]+--hard ]] && block "git reset --hard"
[[ "$CMD" =~ rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*f|rm[[:space:]]+-[a-zA-Z]*f[a-zA-Z]*r ]] && block "rm -rf"
exit 0
```

Register it on `Bash`:

```
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-risky-bash.sh" }
        ]
      }
    ]
  }
}
```

We tested it by piping sample input, for example `echo '{"tool_name":"Bash","tool_input":{"command":"git push -f"}}' | ./block-risky-bash.sh; echo $?`. It returned `2` for `git push --force origin feat`, `git push -f`, `git push origin main`, `git reset --hard HEAD~1`, `rm -rf build` and `rm -fr x`, and `0` for `git push origin feat`, `git push origin feature/main-fix`, `npm test` and `ls -la`. Pattern matching on command text is a speed bump, not a sandbox: a determined command can be written in a way no regex catches. The docs note that a `PreToolUse` deny blocks the call even in `bypassPermissions` mode, which is why this pattern is popular, but pair it with permission rules for anything critical.

## 5. Re-inject key context after compaction

When the context fills up, Claude Code compacts the conversation and details can get lost. A `SessionStart` hook with the `compact` matcher runs after each compaction, and plain text it prints is added to Claude's context:

```
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          { "type": "command", "command": "echo 'Reminder: use pnpm, not npm. Run pnpm test before saying done.'; git log --oneline -5" }
        ]
      }
    ]
  }
}
```

For context you want in every session, `CLAUDE.md` is the better home. Keep this hook for the few lines that must survive compaction.

## 6. Log every shell command Claude runs

Useful for reviewing a long session afterwards:

```
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt" }
        ]
      }
    ]
  }
}
```

## 7. Run a hook only for some commands with `if`

The `if` field uses permission rule syntax, so the hook process starts only when the call matches. This runs a policy script for `git` commands only, not for every Bash call:

```
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git *)",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
          }
        ]
      }
    ]
  }
}
```

`if` works only on tool events (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionDenied`); on any other event it stops the hook from running. The docs call this filter best-effort, so do not use it as your only enforcement.

## When a hook does not fire

* Run `/hooks` and check it is listed under the right event. Matchers are case-sensitive: `Bash`, not `bash`.
* Test the script by hand with sample JSON and check `echo $?`.
* "command not found": use `"$CLAUDE_PROJECT_DIR"` paths, and `chmod +x` the script.
* Invalid JSON in the settings file (a trailing comma, a comment) stops hooks from loading.
* A `Stop` hook that blocks should check `stop_hook_active` in its input and exit early when it is `true`, or Claude Code overrides it after it blocks eight times in a row.

## Going further

Hooks are one layer. `CLAUDE.md` or `AGENTS.md` explains the project, permission rules in `settings.json` enforce hard limits, and hooks automate the rest. Our free [AGENTS.md generator](https://agentkitco.com/tools/agents-md-generator/) builds a starting instruction file, and the [free Agent Skills](https://agentkitco.com/skills/) add reusable workflows such as `prove-it-ran`. If test output is eating your usage, the [Claude Code Usage Saver Kit](https://agentkitco.com/products/claude-code-usage-saver/) includes a hook that keeps only the failing tests.

**Want ready-made files?** See our copy-paste [Claude Code subagent examples](https://agentkitco.com/blog/claude-code-subagents-examples/), [CLAUDE.md examples](https://agentkitco.com/blog/claude-md-examples/) and [custom slash command examples](https://agentkitco.com/blog/claude-code-slash-commands-examples/).

*Written with AI assistance and checked against the official documentation on 2026-10-17. Not affiliated with Anthropic.*

### AI Coding Agent Config Kit

Instruction-file templates for 12 stacks and 12 prompts that write and audit them. 34-page PDF, $14.

[Buy on Gumroad](https://burmunster.gumroad.com/l/agent-config-kit)[See what's inside](https://agentkitco.com/products/agent-config-kit/)

### Not ready to buy? Start with the free PDF.

Get 10 of our most-used prompts as a free PDF from Gumroad. You will be asked for your email so Gumroad can deliver it.

[Get the free 10-prompt PDF](https://burmunster.gumroad.com/l/free-prompts)[Browse 59 prompts here](https://agentkitco.com/free-prompts/)

---

*Originally published at [agentkitco.com](https://agentkitco.com/blog/claude-code-hooks-examples/).*
