# Claude Code hooks: run your own scripts on every edit, command and prompt

A hook is a command Claude Code runs itself at a set moment, such as before a tool call, after a file edit or when Claude finishes. You add hooks in a settings file under "hooks", by event and matcher. Unlike an instruction in CLAUDE.md, a hook runs every time, and a PreToolUse hook that exits with code 2 blocks the action.

- Page: https://tecyfy.com/coding-agents/claude-code/hooks
- Guide: Claude Code (https://tecyfy.com/coding-agents/claude-code), from Anthropic's documentation (https://code.claude.com/docs/en/overview)

Instructions in [CLAUDE.md](/coding-agents/claude-code/project-instructions) are advice: Claude reads them and usually follows them. A hook is not advice. It is a command that Claude Code itself runs at a fixed moment, such as just before Claude runs a tool, just after it edits a file, or when it finishes answering. It runs every time, whatever Claude decides.

Use a hook for anything that must always happen: format each file after an edit, refuse writes to a folder, tell you when Claude is waiting, or put fresh facts into the conversation when a session starts.

## How a hook is set up

Hooks go in a settings file under `"hooks"`, in 3 levels: the **event** (when), a **matcher** (which tool or case) and the **handlers** (what runs). This one runs a formatter after every edit:

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh" }
        ]
      }
    ]
  }
}
```

And the script, `.claude/hooks/format.sh`, made executable with `chmod +x`:

```bash
#!/bin/bash
# Claude Code sends the tool call as JSON on stdin
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
  *.ts|*.tsx|*.js|*.json|*.css) npx prettier --write "$file" ;;
esac
exit 0
```

Put it in `.claude/settings.json` to share it with the team, `.claude/settings.local.json` for yourself in this project, or `~/.claude/settings.json` for every project. Hooks from all the files run together. `$CLAUDE_PROJECT_DIR` is the project's root, so the script is found wherever Claude happens to be working.

Type `/hooks` in a session to see every hook that is loaded and where it came from. It only shows them; you change hooks in the files, and Claude Code picks the change up without a restart.

## Matchers

The matcher says which cases fire the hook. For tool events it is the tool's name:

- `"*"`, `""` or no matcher: every case.
- Plain names such as `Bash` or `Edit|Write`: exactly those names.
- Anything with other characters is a regular expression that may match anywhere in the name, so `Edit.*` also catches `NotebookEdit`. Write `^Edit$` to be exact, and `mcp__github__.*` for every tool of one [MCP server](/coding-agents/claude-code/mcp).

Other events match other things: `SessionStart` matches how the session began (`startup`, `resume`, `clear`, `compact`), `Notification` the kind of notice. Some, such as `Stop` and `UserPromptSubmit`, take no matcher and always fire. The `if` field narrows a handler further with one [permission rule](/coding-agents/claude-code/permissions), such as `"if": "Bash(git push *)"`, so the script starts only for those commands.

## The events you will use most

| Event | Fires | Can block |
| --- | --- | --- |
| `SessionStart` | When a session starts, resumes or is compacted | No |
| `UserPromptSubmit` | When you send a prompt, before Claude sees it | Yes |
| `PreToolUse` | Before a tool runs | Yes |
| `PostToolUse` | After a tool succeeds | No, it already ran |
| `Notification` | When Claude Code needs your attention | No |
| `Stop` | When Claude finishes answering | Yes: Claude keeps going |
| `SubagentStop` | When a [subagent](/coding-agents/claude-code/subagents) finishes | Yes |
| `PreCompact` | Before the conversation is compacted | Yes |

There are over 30 events in all, for permissions, tasks, worktrees, files changing and more. Search them in the list at the end of this page.

## What a hook receives and returns

Claude Code passes the details as JSON on the hook's standard input: the session id, the working folder, the permission mode, the event's name and, for tool events, `tool_name` and `tool_input`, such as the file path or the command.

The hook answers with its exit code:

- **0**: carry on. For `SessionStart` and `UserPromptSubmit`, whatever it prints is added to the conversation for Claude to read.
- **2**: stop. What it writes to stderr becomes the reason, which Claude sees. A `PreToolUse` hook cancels the tool call this way; a `Stop` hook makes Claude keep working.
- **Anything else**: an error that is reported and otherwise ignored, and the action goes ahead.

For finer control a hook can print a JSON object instead, for example to deny a tool call with a reason, or to ask you rather than decide.

## Example: keep Claude out of a folder

Applied database migrations must never be edited. This `PreToolUse` hook refuses any edit to `db/migrations/`:

```bash
#!/bin/bash
# .claude/hooks/protect-migrations.sh
file=$(jq -r '.tool_input.file_path // empty')
if [[ "$file" == *"/db/migrations/"* ]]; then
  echo "Applied migrations are read-only. Write a new migration instead." >&2
  exit 2
fi
exit 0
```

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

A deny from `PreToolUse` holds in every permission mode, even when permissions are skipped. The other way does not work: a hook that allows something cannot override a deny rule. A hook checks only the tool it matches, though, so a determined Bash command could still write the file. For a hard limit, add a deny rule or the [sandbox](/coding-agents/claude-code/permissions) as well.

## Example: a notification when Claude is waiting

In `~/.claude/settings.json`, so it works in every project (macOS):

```json
{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          { "type": "command", "command": "osascript -e 'display notification \"Waiting for you\" with title \"Claude Code\"'" }
        ]
      }
    ]
  }
}
```

On Linux, `notify-send "Claude Code" "Waiting for you"` does the same.

## Example: facts back after compaction

A summary can drop details. A `SessionStart` hook with the `compact` matcher runs right after each [Compaction](https://tecyfy.com/concepts#compaction), and what it prints goes back into the conversation:

```json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          { "type": "command", "command": "echo 'Use pnpm, not npm.'; git status --short" }
        ]
      }
    ]
  }
}
```

## Things that trip people up

- **Several hooks on one event run at the same time**, not in order. If any one blocks, the action is blocked, but the others have still run.
- **`Stop` fires every time Claude finishes answering**, not only when the whole task is done. A Stop hook that blocks keeps Claude working; after 8 blocks in a row Claude Code overrides it. Check the `stop_hook_active` field in the input and let Claude stop if it is already `true`.
- **Timeouts**: a command hook may take up to 10 minutes by default (30 seconds on `UserPromptSubmit`). Set `"timeout"` in seconds for anything slow.
- **Not firing?** Check `/hooks`, that the matcher's case matches the tool name, and that the script is executable. Test it by hand: `echo '{"tool_input":{"file_path":"db/migrations/1.sql"}}' | .claude/hooks/protect-migrations.sh; echo $?`.
- Besides commands, a hook can call a web address (`"type": "http"`), an MCP tool, or have a Claude model judge the case and answer yes or no (`"type": "prompt"`).

## Hooks run your code: check them first

A command hook runs with your full user permissions, not inside Claude's permission rules. A repository's `.claude/settings.json` can define hooks, so cloning a project and opening Claude Code in it can run its code. In an interactive session, Claude Code holds back a folder's hooks until you trust it. Read the file before you say yes.

Scripts that run `claude -p` in a repository you did not write should add `--bare`, which skips hooks and other settings found on disk. In your own hook scripts, put variables in quotes (`"$file"`), use full paths, and never pass text from the tool call straight to a shell.

`"disableAllHooks": true` in your settings turns off every hook except those an organization manages.

| Hook event | Group | What it does |
| --- | --- | --- |
| `SessionStart` | Session | Runs when a session starts, resumes, is cleared or is compacted. What the hook prints is added to the conversation, so it suits loading fresh facts such as the Git status. Example: `"matcher": "startup\|compact"` |
| `SessionEnd` | Session | Runs when a session ends, for cleanup or logging. It cannot stop the session, and its hooks share a short time budget. Example: `"matcher": "logout"` |
| `Setup` | Session | Runs once for preparation when you start Claude Code with --init-only, or with --init or --maintenance in -p mode, such as in CI. Example: `"matcher": "init"` |
| `ConfigChange` | Session | Runs when a settings file or skill changes during a session. Exit 2 stops the change taking effect, except for managed settings. Example: `"matcher": "project_settings"` |
| `PreModelSwitch` | Session | Runs before a model switch you asked for is applied. Exit 2 blocks the switch. The matcher is the model being switched to. Example: `"matcher": ".*opus.*"` |
| `PostModelSwitch` | Session | Runs after the session's model has changed, including changes Claude Code makes itself. What it prints is added to the conversation. |
| `UserPromptSubmit` | Prompt | Runs when you send a prompt, before Claude sees it. It can add context, or exit 2 so the prompt never reaches Claude. Takes no matcher. |
| `UserPromptExpansion` | Prompt | Runs when a command you typed turns into a prompt, before it reaches Claude. Exit 2 blocks it. The matcher is the command's name. Example: `"matcher": "deploy"` |
| `Notification` | Prompt | Runs when Claude Code wants your attention, such as a permission prompt or Claude waiting for input. The usual place for a desktop notification. Example: `"matcher": "permission_prompt\|idle_prompt"` |
| `MessageDisplay` | Prompt | Runs while Claude's answer text is shown. Takes no matcher. |
| `Stop` | Prompt | Runs each time Claude finishes answering, not only at the end of a task. Exit 2 makes Claude keep working; check stop_hook_active so it can stop in the end. |
| `StopFailure` | Prompt | Runs when a turn ends because of an API error, such as a rate limit. Its output and exit code are ignored, so use it for logging or alerts. Example: `"matcher": "rate_limit"` |
| `PreToolUse` | Tools | Runs before a tool call. Exit 2, or a JSON deny, cancels it in every permission mode. It can also ask you, or change the tool's input. Example: `"matcher": "Bash"` |
| `PostToolUse` | Tools | Runs after a tool call succeeds, such as to format a file Claude just edited. The tool has already run, so it cannot be undone. Example: `"matcher": "Edit\|Write"` |
| `PostToolUseFailure` | Tools | Runs after a tool call fails. What the hook writes to stderr is shown to Claude. Example: `"matcher": "Bash"` |
| `PostToolBatch` | Tools | Runs once a batch of tool calls made together has finished, before Claude's next request. Exit 2 stops the loop there. Takes no matcher. |
| `PermissionRequest` | Tools | Runs when Claude Code is about to ask you to allow a tool call. A JSON decision can allow or deny it for you; exit 2 has no effect here. Example: `"matcher": "Bash"` |
| `PermissionDenied` | Tools | Runs when auto mode denies a tool call. It cannot undo the denial, but its JSON can tell Claude it may try again. Example: `"matcher": "Bash"` |
| `Elicitation` | Tools | Runs when an MCP server asks you for input during a tool call. Exit 2 declines without showing a dialog. The matcher is the server's name. Example: `"matcher": "github"` |
| `ElicitationResult` | Tools | Runs after you answer an MCP server's request, before the answer is sent back. Exit 2 turns the answer into a decline. Example: `"matcher": "github"` |
| `SubagentStart` | Agents | Runs when a subagent starts. The matcher is the subagent's type, such as Explore or one of your own. Example: `"matcher": "Explore"` |
| `SubagentStop` | Agents | Runs when a subagent finishes. Exit 2 makes it keep working. Example: `"matcher": "test-runner"` |
| `TaskCreated` | Agents | Runs when a task is being added to the task list. Exit 2 takes the task back out. |
| `TaskCompleted` | Agents | Runs when a task is about to be marked done. Exit 2 keeps it open, such as until the tests pass. |
| `TeammateIdle` | Agents | Runs when a teammate in an agent team is about to go idle. Exit 2 keeps it working. |
| `PreCompact` | Context | Runs before the conversation is compacted, by you or automatically. Exit 2 blocks the compaction. Example: `"matcher": "auto"` |
| `PostCompact` | Context | Runs after compaction finishes. To put facts back into the conversation, use SessionStart with the compact matcher. Example: `"matcher": "manual"` |
| `InstructionsLoaded` | Context | Runs when a CLAUDE.md or .claude/rules file is loaded, at the start or later in the session. Useful to log which instructions applied. Example: `"matcher": "path_glob_match"` |
| `FileChanged` | Files | Runs when a file you name in the matcher changes on disk, such as reloading the environment when .envrc changes. Example: `"matcher": ".envrc\|.env"` |
| `CwdChanged` | Files | Runs when the working folder changes, such as after Claude runs cd. Suits tools like direnv. Takes no matcher. |
| `DirectoryAdded` | Files | Runs when a folder is added to the session, such as with /add-dir. Example: `"matcher": "slash_command"` |
| `WorktreeCreate` | Files | Runs when a Git worktree is being created, and replaces Claude Code's own way of making it. Any non-zero exit makes the creation fail. |
| `WorktreeRemove` | Files | Runs when a worktree a WorktreeCreate hook made is being removed. |

## Questions

### What are hooks in Claude Code?

Commands, web requests or prompts that Claude Code runs at fixed points in a session, such as before a tool runs, after a file is edited, when you send a prompt or when Claude stops. Claude Code runs them itself, so they happen every time rather than when Claude remembers to.

### How do I block a command with a hook?

Add a PreToolUse hook with a matcher such as Bash. Your script reads the tool call as JSON on its input, and if the command should not run, writes the reason to stderr and exits with code 2. Claude Code cancels the call and shows Claude the reason.

### Where do I put Claude Code hooks?

In the hooks block of a settings file - ~/.claude/settings.json for all your projects, .claude/settings.json to share with the team, or .claude/settings.local.json for yourself in one project. Plugins, skills and subagents can also carry hooks. Run /hooks to see every hook that is loaded.

### What is the difference between a hook and CLAUDE.md?

CLAUDE.md is advice Claude reads and usually follows. A hook is a program Claude Code runs whatever Claude decides, so use it for things that must happen every time, such as formatting after each edit or refusing writes to a folder.

### Are hooks safe?

A command hook runs with your full user permissions, so a hook in a repository you clone is code that runs on your machine. Claude Code asks you to trust a folder before its hooks run in an interactive session; read .claude/settings.json first, and in scripts with -p use --bare to skip a repository's hooks.

## Sources

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