Языки: EN RU

Hooks — Lifecycle Events

If skills and subagents extend what Claude can do, hooks control how it does it — and they do so deterministically. A hook is an ordinary shell command (or HTTP request, MCP tool, or even an LLM call) that Claude Code runs automatically at specific moments in the session lifecycle. The model has no say in this decision: a hook always fires, regardless of what Claude thinks.

This is a fundamental difference from instructions in CLAUDE.md or skills. Writing "always run prettier after editing files" is a request that Claude may ignore deep in a long tool chain. A hook with the same rule is a guarantee.


Anatomy of a Hook

Hooks live under "hooks" inside any settings.json (user-level, project-level, or local). The structure has three levels:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}
  • Top-level key — the event name (PostToolUse, PreToolUse, SessionStart, etc.)
  • Matcher (matcher) — a filter specifying which tools or situations to apply the hook to. "Edit|Write" targets only those two tools; "Bash(rm *)" targets only bash commands containing rm; "mcp__github__.*" targets all tools on the GitHub MCP server.
  • hooks array — the list of commands executed sequentially when the matcher fires.

Hooks can be committed to the repository (.claude/settings.json) so the whole team receives them alongside the code. Personal rules (paths to local scripts, tokens) go in .claude/settings.local.json, which is added to .gitignore.

flowchart TD A[Session opens] --> B[[SessionStart]] B --> C[User enters prompt] C --> D[[UserPromptSubmit]] D --> E{Agent loop} E --> F[[PreToolUse]] F --> G{Blocked?} G -- no --> H[Tool executes] G -- yes --> E H --> I[[PostToolUse]] I --> E E --> J[[Stop]] J --> K{Stop?} K -- no --> E K -- yes --> L[Response ready] L --> C C --> M[[SessionEnd]]
flowchart TD
    A[Session opens] --> B[[SessionStart]]
    B --> C[User enters prompt]
    C --> D[[UserPromptSubmit]]
    D --> E{Agent loop}
    E --> F[[PreToolUse]]
    F --> G{Blocked?}
    G -- no --> H[Tool executes]
    G -- yes --> E
    H --> I[[PostToolUse]]
    I --> E
    E --> J[[Stop]]
    J --> K{Stop?}
    K -- no --> E
    K -- yes --> L[Response ready]
    L --> C
    C --> M[[SessionEnd]]
Hook firing order in the Claude Code lifecycle

Event Types

Events fall into three rhythms:

Session-level (once per session):

  • SessionStart — fires immediately after a session is opened or resumed. Useful for loading the environment and fetching up-to-date data.
  • SessionEnd — fires on close. Good for logging and cleaning up temporary files.
  • Setup — fires during init or maintenance.

Per-prompt (once per user request):

  • UserPromptSubmit — fires right after the prompt is submitted, before Claude processes it. Can block or modify the prompt.
  • Stop — fires when Claude is about to finish its response. Can force it to continue.

Agent loop (on every tool call):

  • PreToolUse — fires before execution. The most powerful event: you can block the call, modify its arguments, or log it.
  • PostToolUse — fires after execution. The tool has already run; you can inject additional context or trigger formatting.
Проверь себя
A PreToolUse hook fired on a Bash tool call and returned exit code 2. Bash was supposed to run `git push`. What happens to the command?

How a Hook Communicates with Claude Code

The hook script receives a JSON context object on stdin. For PreToolUse it looks roughly like this:

{
  "session_id": "abc123",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf ./dist"
  },
  "cwd": "/home/user/myproject",
  "permission_mode": "default"
}

The hook returns its decision via an exit code and JSON on stdout:

Exit codeMeaning
0Success. stdout is parsed as JSON containing instructions.
2Blocking error. stderr is passed to Claude as context.
Any otherNon-blocking error; the session continues and a warning is written to the log.

Critically: JSON on stdout is only processed when the exit code is 0. If the script exits with code 1, its output is ignored.

Проверь себя
A hook returned exit 0 and JSON with `permissionDecision: "deny"` in PostToolUse. The Edit tool has already written the file. Will it be blocked?

Blocking and Modifying

The most common PreToolUse scenario is blocking a dangerous call:

#!/bin/bash
# .claude/hooks/block-destructive.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

if echo "$COMMAND" | grep -qE 'rm -rf|DROP TABLE|truncate'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by project policy"
    }
  }'
else
  exit 0
fi

When exiting with exit 0 and no JSON output, Claude Code simply continues. A hook only needs to write to stdout when it wants to influence behavior.

The second option is not to block, but to modify the tool's arguments. The updatedInput field replaces what the tool actually receives:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": { "command": "npm run build -- --no-cache" }
  }
}

For PostToolUse, blocking is unavailable (the tool has already run), but you can inject additional context that Claude will see alongside the result:

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "This file is auto-generated. Edit src/schema.ts and run npm run generate."
  }
}

Three Practical Patterns

1. Auto-formatting after editing

The classic case: Claude writes code, and a hook immediately formats the file.

#!/bin/bash
# .claude/hooks/auto-format.sh
FILE=$(cat | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" =~ \.(ts|tsx|js|jsx)$ ]]; then
  prettier --write "$FILE" 2>/dev/null
fi
exit 0
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/auto-format.sh", "timeout": 15 }]
    }]
  }
}

2. Lint gate: making Claude fix what it broke

PostToolUse cannot undo a tool that has already run, but exit code 2 passes the error back to Claude:

#!/bin/bash
# .claude/hooks/lint-gate.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ "$FILE" =~ \.ts$ ]]; then
  if ! npx tsc --noEmit --skipLibCheck "$FILE" 2>/dev/null; then
    npx tsc --noEmit --skipLibCheck "$FILE" >&2
    exit 2
  fi
fi
exit 0

With exit 2, Claude Code passes stderr back to the model. Claude sees the TypeScript errors and typically fixes them right away.

3. Audit: logging all agent actions

#!/bin/bash
# ~/.claude/hooks/audit.sh
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $(cat | jq -c '.')" \
  >> ~/.claude/audit.jsonl
exit 0
{
  "hooks": {
    "PreToolUse": [{
      "hooks": [{
        "type": "command",
        "command": "${HOME}/.claude/hooks/audit.sh",
        "async": true
      }]
    }]
  }
}

The "async": true field runs the script in the background without blocking the agent loop — ideal for logging where latency is not acceptable.

Проверь себя
A hook with `async: true` exited with exit code 2. Its stderr contains linter errors. Will Claude see those errors?

SessionStart: Preparing the Environment

SessionStart is the only event that can inject context before the first prompt. This makes it indispensable for loading up-to-date data:

#!/bin/bash
# .claude/hooks/session-start.sh
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
UNCOMMITTED=$(git status --short 2>/dev/null | wc -l | tr -d ' ')
LAST_COMMIT=$(git log --oneline -1 2>/dev/null || echo "no commits")

jq -n \
  --arg branch "$BRANCH" \
  --arg uncommitted "$UNCOMMITTED" \
  --arg last "$LAST_COMMIT" '{
  hookSpecificOutput: {
    hookEventName: "SessionStart",
    additionalContext: ("Branch: " + $branch + "\nUncommitted files: " + $uncommitted + "\nLast commit: " + $last),
    sessionTitle: $branch
  }
}'

Claude starts the session already aware of the repository context — no unnecessary questions or git status calls in the first message.

Beyond additionalContext, SessionStart can also:

  • Set environment variables via CLAUDE_ENV_FILE
  • Set the session title (sessionTitle)
  • Reload skills (reloadSkills: true) — useful when skills are stored in git and updated regularly

Hook Types Beyond command

The documentation describes five types; command is just the most common:

TypeWhat it does
commandA shell script. Reads from stdin, writes to stdout.
httpSends a POST request to an endpoint. The body is the same JSON context.
mcp_toolCalls a tool on a connected MCP server.
promptA single-turn LLM request that returns a yes/no decision.
agentLaunches a subagent with tool access for complex validation.

The http hook is particularly useful for teams: a centralized server receives all events, logs them, and enforces policies — with no need to install scripts on every machine.


Viewing All Hooks

The /hooks command in Claude Code opens a read-only hook browser — displaying all configured rules along with their events, matchers, types, and sources (User / Project / Local / Plugin). It is invaluable for debugging: you can immediately see which hook will fire on which event and whether there are any conflicting rules from different levels of the hierarchy.



Быстрое повторение
Чем exit код 2 в хуке отличается от других ошибочных кодов (1, 3, ...)?
Быстрое повторение
В чём главное различие между PreToolUse и PostToolUse?
Быстрое повторение
Почему хук более надёжен, чем инструкция «всегда запускай prettier» в CLAUDE.md?

See also

  • Skills — переносимые навыки — skills give Claude instructions; hooks enforce execution — together they cover the majority of customization scenarios
  • Слэш-команды: встроенные и кастомные — lightweight prompt templates; hooks complement them with deterministic behavior
  • Субагенты и контекстная изоляция — an agent-type hook itself launches a subagent
  • Plugins и marketplace — a plugin bundles hooks together with commands and skills into a single installable unit
  • Что выбрать: команда, навык, субагент, MCP или хук — the complete decision matrix for choosing an extension mechanism
  • Модель разрешений, безопасность и доверие — hooks interact with the permissions system; PreToolUse can expand or restrict it
  • Настройки и иерархия конфигурации — hooks live in settings.json; priority is inherited through the same hierarchy
  • Headless-режим и скриптинг через CLI — hooks work identically in headless mode, opening the door to full automation

Источники

  1. Claude Code Hooks — официальная документация