Мастер Claude · Блок 5. Hooks

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

Практика: hook-система для аудита действий

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

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

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

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

Вы отправили Claude разбираться с папкой входящих: переместить документы по категориям, переименовать по шаблону, выбросить дубли. Он поработал минут пять, сказал «готово». Вы смотрите на результат — порядок есть, но что именно произошло? Какой файл удалён? Какое переименование сделано? Через три дня коллеги спрашивают про договор с подрядчиком, который «был в папке» — и вы не знаете, куда он делся и был ли он вообще тронут.

Директор по развитию работает с документами, которые имеют юридическое и финансовое значение. Письма с партнёрами, аналитические отчёты, данные по сделкам. Когда Claude читает, редактирует или создаёт эти файлы — вам нужна трассировка. Не потому что Claude делает что-то плохое — а потому что без трассировки вы не можете проверить, объяснить или воспроизвести то, что произошло. Это базовая гигиена работы с любым автоматическим инструментом.

На этом уроке мы строим систему из трёх hooks, которые работают вместе. Первый перехватывает файловые операции (Edit и Write), второй — команды в терминале (Bash), третий — конец сессии (SessionEnd). По завершении сессии скрипт собирает всё в читаемый отчёт и кладёт его в папку ~/logs/. Вы будете видеть не только «что сделал Claude», но и когда, в каком порядке, с какими именно файлами.

Что это такое

Представьте журнал в ресторанной кухне. Повар не ведёт его сам — он готовит. Но на каждой станции стоит система, которая автоматически фиксирует: в 18:23 заказ №47, стейк medium, вышел на раздачу. К концу смены у шеф-повара есть полный лог — сколько блюд, какие, когда, были ли задержки. Это не мешает работе кухни, это идёт параллельно.

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

В нашем случае три типа событий нас интересуют:

PostToolUse — событие после того, как Claude использовал инструмент. Мы будем ловить два инструмента: Write и Edit (файловые операции) и Bash (команды в терминале). После каждого вызова — запись в лог-файл.

SessionEnd — событие в конце сессии. Claude завершил работу, сессия закрывается. В этот момент запускается скрипт, который читает накопленный лог и делает из него читаемый отчёт.

PostToolUse — самое полезное событие для аудита

Это событие содержит всё: название инструмента, входные параметры (что передали), выходные данные (что вернулось), время. Для файловых операций — это путь к файлу и содержимое. Для Bash — это команда и вывод. Именно поэтому PostToolUse — основа большинства систем аудита.

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

Нам нужно три вещи: два скрипта Python и конфигурация в settings.json. Разберём по порядку.

Шаг 1. Создаём структуру папок

Сначала убедитесь, что нужные папки существуют:

mkdir -p ~/logs
mkdir -p ~/.claude/hooks

~/logs — сюда будут падать итоговые отчёты после каждой сессии. ~/.claude/hooks — сюда кладём скрипты, которые вызывают hooks.

Шаг 2. Скрипт для логирования операций

Создайте файл ~/.claude/hooks/log-tool-use.py:

#!/usr/bin/env python3
import sys
import json
import os
from datetime import datetime

# Читаем событие из stdin
event = json.load(sys.stdin)

tool_name = event.get("tool_name", "unknown")
tool_input = event.get("tool_input", {})
tool_response = event.get("tool_response", {})

# Формируем запись
entry = {
    "timestamp": datetime.now().isoformat(),
    "tool": tool_name,
}

# Для файловых операций — фиксируем путь
if tool_name in ("Write", "Edit"):
    entry["file"] = tool_input.get("file_path", "")
    entry["action"] = tool_name.lower()
    # Для Edit дополнительно фиксируем, что именно менялось (первые 100 символов)
    if tool_name == "Edit":
        old = tool_input.get("old_string", "")
        new = tool_input.get("new_string", "")
        entry["summary"] = f"replaced {len(old)} chars with {len(new)} chars"

# Для Bash — фиксируем команду
if tool_name == "Bash":
    entry["command"] = tool_input.get("command", "")[:200]  # обрезаем длинные команды
    # Фиксируем код возврата если есть
    if isinstance(tool_response, dict):
        entry["exit_code"] = tool_response.get("returncode", 0)

# Пишем в лог-файл. Один файл на сессию — используем PID процесса как ID сессии
session_id = event.get("session_id", "unknown")
log_file = f"/tmp/claude-session-{session_id}.jsonl"

with open(log_file, "a") as f:
    f.write(json.dumps(entry, ensure_ascii=False) + "\n")

Сделайте скрипт исполняемым:

chmod +x ~/.claude/hooks/log-tool-use.py

Что здесь происходит: скрипт получает на stdin JSON с описанием события. Разбирает его: если это файловая операция — запоминает путь и тип операции, если это Bash — запоминает команду. Пишет запись в файл в /tmp/, используя ID сессии как уникальный идентификатор. Это гарантирует, что разные параллельные сессии не перемешиваются.

Идентификатор сессии приходит в JSON

Идентификатор текущей сессии лежит в поле session_id того же JSON, который хук читает со стандартного входа. Переменной окружения CLAUDE_SESSION_ID не существует — берите значение из события. Используйте его как ключ для изоляции данных между сессиями.

Шаг 3. Скрипт для итогового отчёта

Создайте файл ~/.claude/hooks/session-report.py:

#!/usr/bin/env python3
import sys
import json
import os
from datetime import datetime
from collections import defaultdict

# Читаем событие сессии (содержит session_id)
event = json.load(sys.stdin)
session_id = event.get("session_id", "")

# Ищем лог-файл этой сессии
log_file = f"/tmp/claude-session-{session_id}.jsonl"

if not os.path.exists(log_file):
    # Нет лога — нет отчёта
    sys.exit(0)

# Читаем записи
entries = []
with open(log_file) as f:
    for line in f:
        line = line.strip()
        if line:
            try:
                entries.append(json.loads(line))
            except json.JSONDecodeError:
                pass

if not entries:
    sys.exit(0)

# Анализируем
files_written = []
files_edited = []
bash_commands = []

for e in entries:
    tool = e.get("tool", "")
    if tool == "Write":
        files_written.append(e.get("file", ""))
    elif tool == "Edit":
        files_edited.append(e.get("file", ""))
    elif tool == "Bash":
        bash_commands.append(e.get("command", ""))

# Считаем уникальные файлы
all_files = list(dict.fromkeys(files_written + files_edited))

# Формируем отчёт
report_date = datetime.now().strftime("%Y-%m-%d")
report_time = datetime.now().strftime("%H:%M")
report_file = os.path.expanduser(f"~/logs/session-{report_date}-{session_id[:8]}.md")

with open(report_file, "w") as f:
    f.write(f"# Отчёт сессии Claude\n\n")
    f.write(f"**Дата:** {report_date} {report_time}\n")
    f.write(f"**ID сессии:** {session_id[:8]}...\n")
    f.write(f"**Всего операций:** {len(entries)}\n\n")

    if all_files:
        f.write(f"## Файлы ({len(all_files)} уникальных)\n\n")
        for fpath in all_files:
            ops = []
            if fpath in files_written:
                ops.append("создан/перезаписан")
            if fpath in files_edited:
                ops.append("изменён")
            f.write(f"- `{fpath}` — {', '.join(ops)}\n")
        f.write("\n")

    if bash_commands:
        f.write(f"## Команды терминала ({len(bash_commands)})\n\n")
        for cmd in bash_commands:
            f.write(f"- `{cmd}`\n")
        f.write("\n")

    f.write(f"## Хронология\n\n")
    for e in entries:
        ts = e.get("timestamp", "")[:19].replace("T", " ")
        tool = e.get("tool", "")
        if tool in ("Write", "Edit"):
            detail = e.get("file", "")
            summary = e.get("summary", "")
            line = f"- `{ts}` **{tool}** → `{detail}`"
            if summary:
                line += f" ({summary})"
            f.write(line + "\n")
        elif tool == "Bash":
            cmd = e.get("command", "")[:80]
            f.write(f"- `{ts}` **Bash** → `{cmd}`\n")

# Удаляем временный лог
os.remove(log_file)

print(f"Отчёт сохранён: {report_file}")

Сделайте исполняемым:

chmod +x ~/.claude/hooks/session-report.py

Шаг 4. Подключаем hooks в settings.json

Откройте файл ~/.claude/settings.json (или создайте, если не существует) и добавьте секцию hooks:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/log-tool-use.py"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/log-tool-use.py"
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/session-report.py",
            "timeout": 15
          }
        ]
      }
    ]
  }
}

Если settings.json уже существует — не перезаписывайте весь файл

Откройте существующий файл и добавьте только секцию "hooks" внутрь уже существующего объекта. Если перезапишете весь файл, потеряете другие настройки (разрешения, модель, память).

После сохранения изменения вступают в силу немедленно — следующая сессия Claude уже будет логироваться.

Шаг 5. Проверяем работу

Запустите Claude и попросите его что-нибудь сделать с файлом:

напиши в /tmp/test-audit.txt текст "проверка аудита"

После завершения сессии в ~/logs/ должен появиться файл отчёта. Проверьте:

ls ~/logs/
cat ~/logs/session-*.md

Вы увидите отчёт с хронологией операций, списком затронутых файлов и командами терминала.

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

Ошибка 1: скрипт вызван напрямую, но не имеет прав на выполнение.
Если в command вы пишете путь к скрипту без python3, нужен и шебанг, и бит исполнения — иначе permission denied. При вызове через python3 <путь>, как в конфигурации выше, chmod +x не требуется. Причина: забыли chmod +x. Решение: chmod +x ~/.claude/hooks/log-tool-use.py && chmod +x ~/.claude/hooks/session-report.py. Проверить можно командой ls -la ~/.claude/hooks/ — у скриптов должен быть флаг x.

Ошибка 2: settings.json сломан из-за неправильного JSON.
Симптом: Claude при старте выдаёт ошибку или игнорирует hooks. Причина: лишняя запятая, незакрытая скобка, неправильные кавычки. JSON не прощает синтаксических ошибок. Решение: проверьте JSON онлайн-валидатором (jsonlint.com) или командой python3 -m json.tool ~/.claude/settings.json. Если файл невалидный — исправьте и перезапустите Claude.

Ошибка 3: лог-файл не находится при генерации отчёта.
Симптом: скрипт session-report.py молча завершается, отчёт не создаётся. Причина: session_id в двух скриптах берётся по-разному — а имена файлов должны совпадать. Решение: в обоих скриптах читайте его одинаково, из поля session_id входного JSON. Отладка: добавьте в log-tool-use.py строку print(f"DEBUG session_id: {session_id}", file=sys.stderr) и посмотрите вывод при следующем запуске.

Ошибка 4: отчёт создаётся, но папки ~/logs/ нет.
Симптом: скрипт падает с FileNotFoundError. Причина: папка не создана. Решение: добавьте в начало session-report.py строку os.makedirs(os.path.expanduser("~/logs"), exist_ok=True). Либо создайте папку один раз вручную: mkdir -p ~/logs.

Отладка hooks через stderr

stderr хука, завершившегося с кодом 0, виден только в отладочном логе — включите его через /debug или claude --debug. Проще писать отладку в собственный файл. Если hook ведёт себя непредсказуемо, добавьте print("DEBUG: ...", file=sys.stderr) в нужных местах и запустите тестовую сессию.

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

Эта система нужна, когда вы регулярно поручаете Claude работу с реальными файлами — не разовые задачи, а регулярный процесс. Разбор входящей документации, обновление отчётов, обработка выгрузок. Когда важно сохранить историю: «что именно делал Claude в прошлый вторник с папкой Договоры».

Нужна, когда работаете в команде и результаты работы Claude нужно объяснять или передавать. Отчёт из ~/logs/ — это готовый артефакт, который можно показать или прикрепить к задаче.

Нужна, когда начинаете замечать расхождения между тем, что ожидали, и тем, что получили. Аудитный лог помогает найти, где именно расходятся ожидания и действия.

Не нужна, когда работа разовая и экспериментальная. Если вы просите Claude набросать текст письма или объяснить термин — лог здесь избыточен. Hooks — инфраструктура, которую имеет смысл строить только для повторяющихся процессов.

Не нужна, если Claude работает только с текстом в интерфейсе и не трогает файловую систему. Веб-версия Claude без Claude Code hooks не поддерживает — это функция именно Claude Code (CLI).

Hooks работают только в Claude Code (CLI), не в браузере

Всё, что мы построили сегодня, — это конфигурация CLI-инструмента. Если вы используете Claude через браузер на claude.ai — эта система не применима. Hooks — механизм Claude Code, который запускается через терминал командой claude.

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

День 41 — базовое устройство hooks: события, типы, матчеры. Если что-то в синтаксисе settings.json непонятно, начните оттуда. Сегодняшний урок предполагает, что вы знаете разницу между PreToolUse и PostToolUse.

День 45 — command hooks и передача данных через stdin/stdout. Именно этот механизм используется в наших скриптах: событие приходит как JSON на stdin, скрипт его читает через json.load(sys.stdin). День 45 объясняет этот паттерн подробно.

День 49 — hook-уведомления. Если хотите получать не просто файл отчёта, а Telegram-сообщение после каждой сессии — День 49 показывает, как добавить уведомление к SessionEnd hook. Системы дополняют друг друга: один hook пишет файл, другой отправляет сводку в мессенджер.

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

Создайте скрипт ~/.claude/hooks/log-tool-use.py, сделайте его исполняемым, добавьте один PostToolUse hook в settings.json для инструмента Write. Запустите Claude и попросите создать любой тестовый файл. Проверьте, что в /tmp/ появился файл вида claude-session-*.jsonl с записью об операции.

Критерий «выполнено»: команда cat /tmp/claude-session-*.jsonl показывает JSON-запись с полями tool: "Write" и file: "/путь/к/файлу".

Скрипт для отчёта и SessionEnd hook можно добавить потом — главное сначала убедиться, что базовое логирование работает.

Что можно улучшить дальше

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

Фильтрация по папкам. Сейчас мы логируем все файловые операции подряд. Если Claude работает с временными файлами в /tmp/ — они засоряют отчёт. В скрипт log-tool-use.py можно добавить фильтр: пропускать файлы в /tmp/, __pycache__/, .git/. Три строки кода, зато отчёт станет чище.

Группировка по проектам. Если вы работаете с разными проектами — Новострой, отчёты, договоры — можно добавить в отчёт группировку по корневой папке файла. Тогда вместо плоского списка из 40 файлов вы увидите: «Новострой-М: 12 файлов, Финансы: 3 файла, прочее: 5 файлов».

Сигнализация на аномалии. Если за одну сессию было изменено больше 20 файлов — это нестандартная ситуация, о которой стоит знать. В session-report.py можно добавить проверку: если len(all_files) > 20 — добавить в отчёт предупреждение «необычно высокое число операций». Не блокировка, не запрет — просто пометка для вашего внимания при чтении отчёта.

Все эти улучшения — надстройки над тем же паттерном: PostToolUse собирает данные, SessionEnd их обрабатывает. Логика остаётся та же, меняется только что именно фиксировать и как представлять.

Резюме

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