Мастер Claude · Блок 4. Skills и автоматизация

Урок 38 из 93 · ~10 мин чтения

Pre-commit hooks с Claude

Урок пока закрыт

Курс проходится последовательно.

К текущему уроку

Почему это важно именно вам

Представьте: вы диктуете агенту переработать письмо партнёру. Агент старается — правит тон, убирает лишнее, добавляет конкретику. Вы говорите «сохрани», и файл перезаписывается. Потом вы открываете письмо и видите, что агент зачем-то убрал ключевое условие по срокам. Или добавил неуместный абзац про «взаимовыгодное сотрудничество». Ошибка уже в файле, старая версия потеряна, нужно разбираться задним числом.

Проблема не в том, что агент плохой. Проблема в том, что между командой «правь» и действием «файл перезаписан» нет ни одной точки контроля. Вы работаете напрямую на продакшне, без черновика. В разработке это называется «деплоить прямо на прод без ревью» — и разработчики давно придумали, как с этим бороться. Хуки.

PreToolUse hook в Claude Code — это механизм, который срабатывает до того, как агент реально записывает файл. Вы можете вставить туда любую проверку: другой агент оценивает изменение, скрипт проверяет структуру, простое правило фиксирует нарушение. Если проверка прошла — файл сохраняется. Если нет — Claude получает отказ и объяснение. Это не git-хук и не CI/CD — это живая проверка прямо внутри рабочей сессии, до записи на диск.

Что это такое

В Claude Code есть система хуков — точек, в которых можно перехватить действие агента и вставить свою логику. Один из типов называется PreToolUse: он срабатывает перед тем, как Claude использует конкретный инструмент. Инструменты в Claude Code — это Edit, Write, Bash, WebSearch и другие. Нас сейчас интересуют Edit и Write: именно через них агент меняет файлы на диске.

Хорошая аналогия — юридическая виза на документах. В компании бывает так: контракт перед подписанием обязан пройти юридический отдел. Генеральный может дать добро, но без визы юриста документ не выходит. PreToolUse работает так же: агент хочет записать файл, но сначала этот файл проходит через вашу «юридическую проверку». Если там всё хорошо — файл записывается. Если нет — возвращается на доработку.

Важная деталь, которую легко перепутать: это не git pre-commit hook. Git pre-commit — это хук в системе контроля версий, он срабатывает когда вы делаете git commit. То, о чём мы говорим сегодня, — это хук внутри Claude Code, он срабатывает когда Claude пытается сохранить файл в рамках текущей сессии. Git про это ничего не знает. Это принципиально разные вещи на разных уровнях.

Ключевая идея

PreToolUse срабатывает до записи файла — не после. Это не журнал изменений и не откат, это ворота. Агент не может обойти хук, он просто получит отказ вместо подтверждения.

Как работает на практике

Хуки настраиваются в файле settings.json. Он лежит в .claude/settings.json внутри вашего рабочего каталога или в ~/.claude/settings.json для глобальных настроек. Мы работаем с проектным — он влияет только на этот каталог.

Шаг 1. Открыть settings.json

# Проверить, что файл существует
ls .claude/settings.json

Если файла нет — создайте пустой:

{}

Если файл уже есть с другим содержимым — добавим секцию hooks к существующему содержимому.

Шаг 2. Добавить секцию hooks

Базовая структура хука выглядит так:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/doc_review.py"
          }
        ]
      }
    ]
  }
}

Разберём каждое поле:

Где находится .claude/

Папка .claude/ создаётся в корне вашего рабочего каталога, когда вы первый раз запускаете Claude Code там. Если её нет — создайте вручную: mkdir -p .claude/hooks

Шаг 3. Написать скрипт проверки

Создайте файл .claude/hooks/doc_review.py. Этот скрипт будет получать информацию о том, что агент собирается сохранить, и принимать решение.

Вот простой рабочий пример — скрипт проверяет текстовые документы на наличие стоп-слов и слишком коротких версий:

#!/usr/bin/env python3
import sys
import json

# Читаем входные данные от Claude Code
data = json.load(sys.stdin)

tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {})

# Получаем имя файла и новое содержимое
file_path = tool_input.get("file_path", "")
new_content = tool_input.get("content", "") or tool_input.get("new_string", "")

# Проверяем только документы (не код, не конфиги)
doc_extensions = [".md", ".txt", ".docx"]
is_doc = any(file_path.endswith(ext) for ext in doc_extensions)

if not is_doc:
    # Не документ — пропускаем без проверки
    sys.exit(0)

# Список стоп-слов (замените своими)
stop_phrases = [
    "взаимовыгодное сотрудничество",
    "в рамках текущей парадигмы",
    "синергетический эффект",
    "уважаемые партнёры",
]

found_issues = []

for phrase in stop_phrases:
    if phrase.lower() in new_content.lower():
        found_issues.append(f"Стоп-фраза: «{phrase}»")

# Проверка: письмо не должно быть короче 50 слов
word_count = len(new_content.split())
if word_count < 50 and word_count > 0:
    found_issues.append(f"Слишком короткий текст: {word_count} слов (минимум 50)")

if found_issues:
    # Возвращаем ошибку — Claude получит этот текст и будет вынужден исправить
    result = {
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": "Проверка документа не прошла:\n"
                + "\n".join(f"- {issue}" for issue in found_issues)
        }
    }
    print(json.dumps(result))
    sys.exit(0)  # блокировка задаётся полем permissionDecision, код выхода при этом 0

# Всё хорошо — разрешаем сохранение
sys.exit(0)

Что здесь происходит:
- Claude Code передаёт скрипту через stdin JSON с данными об инструменте
- Скрипт проверяет: это документ или нет, есть ли стоп-фразы, нормальная ли длина
- Если проблем нет — sys.exit(0), файл сохраняется
- Если есть проблемы — sys.exit(2) с объяснением в JSON, Claude получает описание проблемы и должен её устранить

exit code имеет значение

sys.exit(0) — разрешить. sys.exit(1) — ошибка скрипта (не блокирует). sys.exit(2) — заблокировать действие и вернуть агенту описание проблемы. Не перепутайте: exit 1 не заблокирует запись, а exit 2 — заблокирует.

Шаг 4. Сделать скрипт исполняемым

chmod +x .claude/hooks/doc_review.py

Шаг 5. Проверить в работе

Запустите сессию Claude Code и попросите что-нибудь записать в файл с расширением .md, включив туда стоп-фразу. Например:

Напиши в файл test.md короткое письмо со словами "взаимовыгодное сотрудничество"

Агент попытается записать файл через Write, хук перехватит попытку, скрипт найдёт стоп-фразу, вернёт блокировку с объяснением. Claude увидит причину отказа и начнёт переписывать — уже без стоп-фразы. Файл запишется только после того, как все проверки пройдут чисто.

Реальный кейс: агент-ревьюер вместо скрипта

Скрипт с жёсткими правилами — это только один вариант. Более мощный подход: хук запускает другой Claude, который читает документ и оценивает его.

#!/usr/bin/env python3
import sys
import json
import subprocess

data = json.load(sys.stdin)
tool_input = data.get("tool_input", {})
file_path = tool_input.get("file_path", "")
new_content = tool_input.get("content", "")

# Проверяем только письма и отчёты
if not any(file_path.endswith(ext) for ext in [".md", ".txt"]):
    sys.exit(0)

# Запускаем Claude как ревьюера
review_prompt = f"""Ты редактор деловых текстов. Оцени этот документ:

{new_content}

Ответь строго в формате JSON:
{{"approved": true/false, "issues": ["проблема 1", "проблема 2"]}}

Блокируй (approved: false) только если:
- Есть явные канцеляризмы и пустые фразы
- Потеряны конкретные факты или числа
- Тон неуместен для делового письма

Не блокируй из-за стиля или личных предпочтений."""

result = subprocess.run(
    ["claude", "-p", review_prompt],
    capture_output=True, text=True, timeout=30
)

try:
    review = json.loads(result.stdout.strip())
    if not review.get("approved", True):
        issues = review.get("issues", [])
        output = {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": "Ревьюер отклонил документ:\n"
                    + "\n".join(f"- {i}" for i in issues)
            }
        }
        print(json.dumps(output))
        sys.exit(2)
except (json.JSONDecodeError, KeyError):
    # Если ревьюер не ответил в нужном формате — пропускаем
    sys.exit(0)

sys.exit(0)

Этот вариант дороже по времени (каждое сохранение добавляет несколько секунд на вызов Claude), но гораздо умнее жёстких правил. Ревьюер понимает контекст, а не просто ищет строки.

Когда использовать скрипт, а когда Claude-ревьюер

Скрипт с правилами: быстро, дёшево, предсказуемо — хорош для формальных требований (стоп-слова, минимальная длина, обязательные поля). Claude-ревьюер: медленнее, стоит токенов, но понимает смысл — хорош для качественных требований (тон, логика, полнота). Начните со скрипта, добавьте Claude-ревьюер только для важных документов.

Частые ошибки

Ошибка 1: Поставить хук на все инструменты подряд

Если написать matcher: ".*", хук будет срабатывать на каждое действие агента — включая чтение файлов, поиск, вызов bash. Это не только замедляет работу, но и ломает многие операции, которые не имеют отношения к документам.

Правильно: matcher: "Edit|Write" — только инструменты записи файлов. Если нужно ограничить дальше — можно добавить проверку расширения файла внутри скрипта, как показано в примере выше.

Ошибка 2: Возвращать exit 1 вместо exit 2

Exit code 1 означает «скрипт завершился с ошибкой». Claude Code интерпретирует это как технический сбой хука, а не как намеренную блокировку. Файл при этом может записаться. Exit code 2 означает именно «заблокировать действие». Перепутать их — значит получить хук, который молча не работает.

Ошибка 3: Блокировать слишком агрессивно

Если хук блокирует любое несовершенство, агент начнёт зацикливаться: переписывает файл, снова получает блокировку, снова переписывает. Через несколько итераций сессия встаёт или Claude начинает игнорировать ревью. Хук должен блокировать реальные проблемы, не эстетические разногласия. Формулируйте критерии блокировки чётко и минималистично.

Ошибка 4: Не обрабатывать файлы, которые хук не должен трогать

Если ваш хук не добавляет проверку «это вообще документ?», он будет запускаться при каждой записи любого файла. Агент создаёт временный JSON-файл с промежуточными данными — хук запускается. Агент обновляет конфиг — хук запускается. Добавьте в начало скрипта явный фильтр по расширению файла и сразу выходите с exit 0, если файл не попадает в вашу зону контроля.

Хук не защита от злого умысла

PreToolUse хук запускается в том же окружении, что и агент. Если что-то пошло не так и хук упал с непредвиденной ошибкой, поведение зависит от настроек. Хук — это удобный инструмент контроля качества, не система безопасности. Для критичных документов всегда используйте версионирование (git) дополнительно к хукам.

Когда нужно / когда нет

PreToolUse хуки нужны, когда:

PreToolUse хуки не нужны, когда:

Хуки как документация ожиданий

Хорошо написанный хук — это заодно и спецификация требований к документу. Когда вы описываете, что должен проверять скрипт, вы явно формулируете, каким должен быть документ. Это полезно само по себе, даже если никакой автоматизации не будет.

Связь с другими уроками

День 41 — система хуков: все типы событий. Сегодня мы работаем с PreToolUse — одним из нескольких типов. В фазе 5 вы разберёте полную картину: PostToolUse, Stop, Notification и другие. PreToolUse — самый понятный вход в тему хуков, после этого урока остальные типы будут логичны.

День 24 — субагенты и их инструменты. Хук может запускать не только скрипт, но и субагента-ревьюера. Если вы разобрались, как устроены субагенты в .claude/agents/, — вы можете использовать их как проверяющую логику в хуке. Это более мощная, но и более дорогая комбинация.

День 36 — fork и параллельные сессии. Когда несколько агентов работают с одними документами в параллельных сессиях, хуки помогают обеспечить единые стандарты качества. Без хуков каждый агент пишет по-своему; с хуком — у всех один фильтр на выходе.

Задание на сегодня

Добавьте в свой рабочий каталог один PreToolUse хук с простой проверкой текстовых файлов.

Конкретные шаги:

  1. Создайте папку .claude/hooks/ если её ещё нет.
  2. Создайте файл .claude/hooks/doc_check.py — возьмите первый пример из урока как основу.
  3. Замените стоп-фразы в скрипте на реальные слова, которые вы сами хотите блокировать (хотя бы три штуки из вашей практики).
  4. Добавьте в .claude/settings.json секцию hooks по шаблону из урока.
  5. Запустите Claude Code и попросите записать в любой .md файл текст с одной из ваших стоп-фраз.
  6. Убедитесь, что агент получил блокировку и переписал текст без стоп-фразы.

Критерий «выполнено»: агент попытался записать файл со стоп-фразой, получил отказ от хука, самостоятельно убрал стоп-фразу и записал исправленную версию. Вы при этом ничего не делали вручную.

Резюме

Следующий урок откроется после отметки.