---
title: "Claude Code: Hooks"
slug: "claude-code-hooks"
category: "ai-agents"
tags: ["claude-code", "hooks", "automation", "settings", "ai-agents", "workflow"]
status: "stable"
last_updated: 2026-05-14
summary: "Configure pre-tool, post-tool, stop, and user-prompt-submit hooks in settings.json to automate formatting, linting, and session teardown."
related: ["[[ai-agents/claude-code]]", "[[ai-agents/claude-code-permissions]]", "[[ai-agents/claude-code-skills]]", "[[ai-agents/claude-code-claude-md]]", "[[tooling/claude-code-workflow]]", "[[howto/set-up-claude-code]]", "[[prompt-engineering/prompt-design]]"]
---

## Overview

Hooks let you attach shell commands to events in a Claude Code session. The harness runs them, not Claude, so they fire reliably regardless of model output. Use hooks for anything that must happen unconditionally: format on write, lint on stop, log on every user message. Configuration lives in `settings.json`. See [[ai-agents/claude-code]] for the broader workflow context.

## Configure hooks in settings.json

All hook configuration sits under the `hooks` key in `.claude/settings.json` (project-level) or `~/.claude/settings.json` (user-level).

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\"" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "npm run lint 2>&1 | tee /tmp/cc-lint.log" }
        ]
      }
    ]
  }
}
```

Project-level settings override user-level for the fields they set. Hooks merge additively: both fire if both define a `Stop` hook.

## PostToolUse hooks run after every matched tool call

`PostToolUse` hooks receive the tool name and input via environment variables. Use them for side-effects that should follow a specific tool.

- `CLAUDE_TOOL_NAME`: the tool that just ran (`Write`, `Edit`, `Bash`, etc.).
- `CLAUDE_TOOL_INPUT_FILE_PATH`: the file path from the tool input, for file tools.

Common uses: auto-format on `Write` or `Edit`, update an index file after `Write`, log every `Bash` command to an audit file.

The matcher is a regex applied to the tool name. `"matcher": "Write|Edit"` fires on both. Omit the matcher to fire on every tool call.

## PreToolUse hooks run before the tool fires

`PreToolUse` hooks can block the tool by exiting non-zero. Use them as safety valves.

```json
{
  "matcher": "Bash",
  "hooks": [
    { "type": "command", "command": "/usr/local/bin/audit-bash \"$CLAUDE_TOOL_INPUT_COMMAND\"" }
  ]
}
```

If the hook exits non-zero, Claude Code cancels the tool call and surfaces the hook's stderr as an error message. This lets you enforce allowlists at the harness level rather than trusting model judgment.

## Stop hooks run when Claude finishes a turn

A `Stop` event fires each time the model stops generating, before control returns to the user. Use stop hooks for session hygiene.

- Run the test suite and append the result to `SESSION_LOG.md`.
- Lint the entire modified file set.
- Push a metrics beacon to a logging endpoint.

Stop hooks run synchronously; a slow hook delays the prompt returning. Keep them under two seconds or run heavy work in the background with `&`.

## UserPromptSubmit hooks run before each user message reaches the model

`UserPromptSubmit` fires after the user presses enter, before the message is sent. Use this hook to inject context automatically.

```json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "echo \"Current branch: $(git rev-parse --abbrev-ref HEAD)\"" }
        ]
      }
    ]
  }
}
```

The hook's stdout is prepended to the user message as a system note. Inject git status, environment name, or session metadata here without cluttering the prompt.

## Use the auto-format pattern for consistent output

Pairing a `PostToolUse` hook on `Write|Edit` with your formatter eliminates the "Claude forgot to format" class of bugs.

```json
"PostToolUse": [
  {
    "matcher": "Write|Edit",
    "hooks": [
      { "type": "command", "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" --log-level silent" }
    ]
  }
]
```

This fires on every file write, including writes Claude makes in multi-step plans. The formatter runs on the actual output, not a copy.

## Use the lint-on-stop pattern to catch regressions early

A `Stop` hook that runs lint turns every turn boundary into a regression check.

```json
"Stop": [
  {
    "hooks": [
      { "type": "command", "command": "npm run lint --silent && echo 'lint: ok' >> .claude/session.log || echo 'lint: FAIL' >> .claude/session.log" }
    ]
  }
]
```

The log entry is cheap. If lint fails, the model's next turn can read the log and self-correct. Combine this with a `UserPromptSubmit` hook that prepends the last lint status to every user message.

## Related

- [[ai-agents/claude-code]]
- [[ai-agents/claude-code-permissions]]
- [[ai-agents/claude-code-skills]]
- [[ai-agents/claude-code-claude-md]]
- [[tooling/claude-code-workflow]]
- [[howto/set-up-claude-code]]
