# Claude Code Hooks: Automate Formatting, Tests, and Guardrails

> How Claude Code hooks work — the major hook events, the settings.json configuration shape, exit codes and JSON output, plus three hooks worth copying.

Hooks are commands Claude Code runs automatically at lifecycle events — before a tool call, after an edit, when a session starts or ends. Unlike CLAUDE.md instructions, hooks execute deterministically every time: format after every edit, block writes to protected paths, get a notification when Claude needs input. You configure them per event under the hooks key in settings.json.

Claude Code hooks are user-defined commands that run automatically at specific points in Claude Code's lifecycle — before a tool call, after a file edit, when you submit a prompt, when the session starts or ends. They are the difference between *asking* the agent to follow a rule and *enforcing* it.

## Instructions ask. Hooks guarantee.

You can write "always run prettier after editing a file" in [CLAUDE.md](/guides/configuration/claude-md-best-practices), and Claude will usually do it. But "usually" is doing real work in that sentence: instructions compete with everything else in context, and in a long session they can be deprioritized or compacted away. A hook removes the model from the loop entirely — the formatter runs after every edit because the harness runs it, not because the model remembered to.

That's the mental model for what belongs where:

- **Judgment calls** (naming, architecture, tone) → CLAUDE.md instructions.
- **Invariants** (formatting, protected files, notifications, audit logs) → hooks.

## What teams actually use hooks for

- **Auto-formatting** — run `prettier`, `ruff`, or `gofmt` on every file Claude edits.
- **Guardrails** — block edits to `.env` files, lockfiles, or migrations; gate dangerous Bash patterns beyond what [permission rules](/guides/configuration/claude-code-settings-permissions) express.
- **Feedback loops** — run the affected tests after an edit and feed failures straight back to Claude.
- **Notifications** — desktop ping when Claude needs input or finishes a long task.
- **Audit and compliance** — log every tool call with its input to a file your team can review.

## The hook events

The core events, and whether they can block the action:

| Event | Fires | Can block? |
| --- | --- | --- |
| `SessionStart` | When a session begins or resumes | No |
| `UserPromptSubmit` | Before Claude processes your prompt | **Yes** |
| `PreToolUse` | Before any tool call executes | **Yes** |
| `PostToolUse` | After a tool call succeeds | No |
| `PostToolUseFailure` | After a tool call fails | No |
| `Notification` | When Claude Code sends a notification (e.g. waiting for input) | No |
| `Stop` | When Claude finishes responding | No |
| `SubagentStop` | When a subagent finishes | No |
| `PreCompact` | Before context compaction | No |
| `SessionEnd` | When the session terminates | No |

More specialized events exist — `PermissionRequest`, `FileChanged`, worktree and subagent lifecycle events, and others — and the list grows with the product; the docs' hooks reference is the source of truth for the full set. The two you'll use most are `PreToolUse` (gate things) and `PostToolUse` (react to things).

## Configuring a hook

Hooks live under the `hooks` key in any settings file — `~/.claude/settings.json` for every project, `.claude/settings.json` to share with your team, `.claude/settings.local.json` to keep personal. Each event maps to a list of matchers, and each matcher to a list of handlers:

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

- **`matcher`** filters which tool triggers the hook — an exact tool name (`Bash`), an alternation (`Edit|Write`), or `*` for everything. MCP tools match by their full name (`mcp__github__create_issue`).
- **`type: "command"`** is the workhorse. Other handler types exist — prompt and agent hooks that ask a model to evaluate a condition, HTTP hooks that POST the event to a URL — but start with commands.
- **`timeout`** caps how long the hook may run, in seconds.

Run `/hooks` in a session to see every registered hook and which settings file it came from, and set `"disableAllHooks": true` to switch them off temporarily.

## How a hook talks back

Every hook receives the event as JSON on stdin:

```json
{
  "session_id": "abc123",
  "cwd": "/path/to/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Edit",
  "tool_input": { "file_path": "/path/to/project/src/index.ts" }
}
```

It answers with its exit code, and optionally with JSON on stdout:

- **Exit 0** — success. Stdout may contain JSON for fine-grained control.
- **Exit 2** — block, for events that support it. For `PreToolUse` the tool call never runs, and your stderr is shown to Claude as the reason.
- **Anything else** — non-blocking error; the session continues.

The JSON output unlocks the precise controls: a `PreToolUse` hook can return `"permissionDecision": "allow" | "ask" | "deny"` with a reason, any hook can add `"additionalContext"` for Claude or a `"systemMessage"` for you, and `"continue": false` stops the session outright.

## Three hooks worth copying

**1. Format after every edit** (`PostToolUse`, matcher `Edit|Write`):

```bash
#!/usr/bin/env bash
# .claude/hooks/format-after-edit.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
  *.ts|*.tsx|*.js|*.jsx) npx prettier --write "$file" >/dev/null 2>&1 ;;
  *.py) ruff format "$file" >/dev/null 2>&1 ;;
esac
exit 0
```

**2. Protect paths Claude should never touch** (`PreToolUse`, matcher `Edit|Write`):

```bash
#!/usr/bin/env bash
# .claude/hooks/protect-paths.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
  *.env*|*/secrets/*|*.pem|*package-lock.json|*pnpm-lock.yaml)
    echo "Blocked: $file is protected. Change it manually if it really must change." >&2
    exit 2 ;;
esac
exit 0
```

Exit code 2 blocks the edit, and the stderr line tells Claude *why*, so it routes around the restriction instead of retrying it.

**3. Desktop notification when Claude needs you** (`Notification`):

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

(macOS; on Linux swap in `notify-send "Claude Code" "Claude needs input"`.) Kick off a long task, go do something else, and let the hook pull you back.

> [!TIP]
> Want a hook but don't want to hand-write the matcher, script, and JSON plumbing? The [hook-writer](/skills/workflow/hook-writer) skill turns "block edits to migrations and notify me when tests fail" into a tested hook config.

## Hooks run as you

A hook is arbitrary code executing with your credentials, inside your session. Treat the mechanism with respect:

- **Review third-party hooks.** A repo's checked-in `.claude/settings.json` can register hooks; read them before working in an untrusted repo, the same way you'd read a `postinstall` script.
- **Quote and validate.** Hook input contains model-chosen values (file paths, commands). Quote every variable and handle unexpected shapes — your protect-paths hook shouldn't be injectable through a weird filename.
- **Decide fail-open vs. fail-closed.** A formatter that errors should probably exit 0 (fail open); a compliance gate should exit 2 on any doubt (fail closed).
- **Keep them fast.** Hooks run inline; a slow `PreToolUse` hook taxes every single tool call.

Hooks are one of the three pillars that make Claude Code programmable — alongside [settings and permissions](/guides/configuration/claude-code-settings-permissions) and [memory](/guides/configuration/claude-code-memory-context). Wire all three and the agent stops being a chat window and starts being infrastructure: [running unattended in CI](/commands/workflow/setup-claude-ci) with the same guardrails you use locally.

---

_Source: https://agentscamp.com/guides/configuration/claude-code-hooks — Guide on AgentsCamp._
