Почему это важно именно вам
Вы отправили 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 их обрабатывает. Логика остаётся та же, меняется только что именно фиксировать и как представлять.
Резюме
- Три hook-а образуют полную систему аудита: PostToolUse для файлов, PostToolUse для Bash, SessionEnd для итогового отчёта
- Каждый hook передаёт событие как JSON на stdin скрипта — читайте через
json.load(sys.stdin) - Используйте поле
session_idиз входного JSON для изоляции данных между сессиями — без него логи разных сессий смешаются - Отчёт в
~/logs/содержит хронологию, список затронутых файлов и команды — это готовый артефакт для проверки работы - Перед добавлением hooks убедитесь, что
settings.jsonвалидный JSON — сломанный файл отключает все hooks разом - Базовую систему можно расширить фильтрацией, группировкой по проектам и сигнализацией на аномалии — не меняя основную архитектуру