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

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

Command hooks: shell-скрипт как реакция

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

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

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

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

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

Это стандартная ситуация для директора, который делегирует агенту работу с документами. Агент эффективен, но непрозрачен. Вы доверяете ему итог, но не контролируете процесс. При разборе полётов «что агент делал в 14:47» — ответа нет. Особенно неприятно, когда нужно объяснить коллеге или согласовать с партнёром: «вот, агент обновил договор» — а какую версию, когда, в каком порядке с другими файлами?

Command hook решает эту проблему напрямую. Это механизм, который запускает ваш скрипт в момент, когда агент что-то делает. Не до действия (это PreToolUse из дня 38) и не вместо него — а параллельно. Агент сохранил файл — ваш скрипт мгновенно добавил строчку в журнал: какой файл, когда, в рамках какой сессии. К концу рабочего дня у вас есть полная хронология работы агента в понятном виде. Это не магия и не сложная интеграция — это один скрипт из 20 строк и пять строк конфига.

Что это такое

В Claude Code хуки — это точки подключения к событиям агента. Мы уже разбирали PreToolUse: хук, который срабатывает до действия и может его заблокировать. Command hook в контексте PostToolUse работает иначе: он запускается после того, как агент выполнил действие, и никак не влияет на результат. Это реакция на событие, а не контроль над ним.

Хорошая аналогия — бортовой регистратор на самолёте. Пилот управляет самолётом как обычно, ничего не меняется в управлении. Но каждое действие фиксируется в чёрном ящике независимо от пилота. Если что-то пойдёт не так — у вас есть полная хронология. Command hook — это ваш чёрный ящик для агента.

Другая аналогия из бизнеса: сигнализация в офисе. Дверь открылась — пришло уведомление. Никто не мешал человеку войти, но факт входа зафиксирован. Command hook работает так же: агент открыл файл, отредактировал, записал — каждое из этих событий может триггерить ваш скрипт. Скрипт делает что угодно: пишет в лог, отправляет уведомление, запускает бэкап, обновляет реестр.

Технически это выглядит так: в конфигурационном файле settings.json вы описываете, на какое событие реагировать и какую команду запускать. Когда событие происходит, Claude Code запускает вашу команду, подаёт ей событие JSON-ом на стандартный вход и ждёт завершения. Скрипт сделал своё дело — сессия продолжается.

Ключевое отличие от PreToolUse

PreToolUse может заблокировать действие агента (exit code 2). PostToolUse + command hook — никогда. Это чистая реакция: действие уже произошло, вы просто узнали об этом. Хотите контролировать — день 38. Хотите наблюдать и фиксировать — сегодняшний урок.

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

Разберём конкретный кейс: делаем журнал изменений файлов. Каждый раз, когда агент редактирует или создаёт файл, в ~/Logs/claude-changes.log появляется строчка: дата, время, имя файла. К концу дня у вас есть полная история.

Шаг 1. Структура конфигурации

Хуки прописываются в .claude/settings.json в корне вашего рабочего каталога. Вот как выглядит секция для command hook:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "/Users/yourname/.claude/hooks/log-changes.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

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

Поле shell

Поле shell в конфигурации command hook выбирает интерпретатор и принимает "bash" или "powershell" — не true. По умолчанию, когда не задано поле args, команда и так передаётся шеллу: пайпы и && работают. Поле args со списком аргументов, наоборот, запускает исполняемый файл напрямую, без шелла.

Шаг 2. Что скрипт получает от Claude Code

Часть контекста приходит через переменные окружения, основное — JSON-ом на стандартный вход:

Помимо этого, доступны все обычные переменные среды: HOME, USER, PATH, PWD.

Разберём, что содержит tool_input для инструмента Edit:

{
  "file_path": "/Users/yourname/projects/report.md",
  "old_string": "старый текст",
  "new_string": "новый текст"
}

Для Write (создание файла):

{
  "file_path": "/Users/yourname/projects/contract.md",
  "content": "полное содержимое файла"
}

Разные ключи в Edit и Write

И у Write, и у Edit путь лежит в tool_input.file_path — поле одно и то же. Различается остальное: у Writecontent, у Editold_string и new_string, просто разные инструменты с разными параметрами. Если ваш скрипт обрабатывает оба — нужно проверять оба ключа. В примере ниже показано, как это сделать правильно.

Шаг 3. Пишем скрипт-логгер

Создайте файл ~/.claude/hooks/log-changes.sh. Используйте домашнюю папку, а не папку проекта — тогда один скрипт будет работать для всех ваших проектов.

#!/bin/bash

# Создаём папку для логов, если её нет
LOG_DIR="$HOME/Logs"
LOG_FILE="$LOG_DIR/claude-changes.log"
mkdir -p "$LOG_DIR"

# Текущее время в читаемом формате
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

# Парсим tool_input — там JSON с параметрами инструмента
# Пробуем оба возможных ключа для пути к файлу
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "
import sys, json
try:
    data = json.load(sys.stdin)
    # путь и у Edit, и у Write лежит в tool_input.file_path
    print(data.get('tool_input', {}).get('file_path', 'unknown'))
except:
    print('unknown')
")

# Имя инструмента (Edit или Write)
TOOL_NAME=$(jq -r '.tool_name // "unknown"' <<<"$INPUT")

# Короткое имя файла (только последняя часть пути)
FILE_NAME=$(basename "$FILE_PATH")

# Записываем строчку в лог
echo "[$TIMESTAMP] $TOOL_NAME: $FILE_NAME ($FILE_PATH)" >> "$LOG_FILE"

# Выходим с 0 — скрипт отработал успешно
exit 0

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

chmod +x ~/.claude/hooks/log-changes.sh

Теперь при каждом сохранении файла агентом в ~/Logs/claude-changes.log будет появляться строчка вида:

[2024-03-15 14:32:07] Edit: contract_v2.md (/Users/yourname/projects/contracts/contract_v2.md)
[2024-03-15 14:33:41] Write: report_q1.md (/Users/yourname/projects/reports/report_q1.md)
[2024-03-15 14:35:12] Edit: contract_v2.md (/Users/yourname/projects/contracts/contract_v2.md)

Шаг 4. Расширяем скрипт — добавляем проект

Если вы работаете с несколькими проектами, полезно добавить в лог название проекта. CLAUDE_PROJECT_DIR содержит путь к корню проекта — возьмём последнее имя папки.

#!/bin/bash

LOG_DIR="$HOME/Logs"
LOG_FILE="$LOG_DIR/claude-changes.log"
mkdir -p "$LOG_DIR"

TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "
import sys, json
try:
    data = json.load(sys.stdin)
    print(data.get('tool_input', {}).get('file_path', 'unknown'))
except:
    print('unknown')
")

# Название проекта из CLAUDE_PROJECT_DIR
PROJECT_NAME=$(basename "${CLAUDE_PROJECT_DIR:-unknown}")
FILE_NAME=$(basename "$FILE_PATH")

echo "[$TIMESTAMP] [$PROJECT_NAME] $FILE_NAME" >> "$LOG_FILE"

exit 0

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

Запустите Claude Code в любом рабочем каталоге и попросите что-нибудь записать:

Создай файл test-hook.md с одной строкой "Проверка хука"

После выполнения откройте лог:

cat ~/Logs/claude-changes.log

Должна появиться строчка с именем файла и временем. Если лог пуст — проверьте: создан ли скрипт, выставлен ли chmod +x, указан ли правильный путь в settings.json.

Глобальный vs проектный settings.json

Если хотите, чтобы журнал вёлся для всех ваших проектов, прописывайте хук в ~/.claude/settings.json (глобальный). Если только для одного проекта — в .claude/settings.json внутри папки проекта. Глобальный применяется всегда, проектный — только когда Claude Code запущен из этой папки. Правила суммируются: если хук прописан и там, и там — сработают оба.

Расширенный кейс: уведомление в Telegram

Журнал в файле — хорошо. Уведомление в Telegram сразу после изменения — ещё лучше, если вы хотите следить за работой агента в реальном времени или запустили его на длинный пакетный прогон.

#!/bin/bash

LOG_DIR="$HOME/Logs"
LOG_FILE="$LOG_DIR/claude-changes.log"
mkdir -p "$LOG_DIR"

TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "
import sys, json
try:
    data = json.load(sys.stdin)
    print(data.get('tool_input', {}).get('file_path', 'unknown'))
except:
    print('unknown')
")

FILE_NAME=$(basename "$FILE_PATH")
PROJECT_NAME=$(basename "${CLAUDE_PROJECT_DIR:-unknown}")

# Пишем в лог всегда
echo "[$TIMESTAMP] [$PROJECT_NAME] $FILE_NAME" >> "$LOG_FILE"

# Отправляем в Telegram только для папки reports/
# (чтобы не спамить на каждый технический файл)
if echo "$FILE_PATH" | grep -q "/reports/"; then
    TG_TOKEN="ваш_токен_бота"
    TG_CHAT_ID="ваш_chat_id"
    MESSAGE="Агент обновил: $FILE_NAME ($PROJECT_NAME)"

    curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
        -d "chat_id=${TG_CHAT_ID}" \
        -d "text=${MESSAGE}" \
        > /dev/null 2>&1
fi

exit 0

Фильтр grep -q "/reports/" — чтобы уведомления приходили только при изменении файлов в папке reports/. Без такого фильтра вы получите уведомление на каждый временный и технический файл, который агент создаёт в процессе работы.

Токены и секреты в скриптах

Не хардкодьте токены прямо в тело скрипта, если скрипт лежит в папке, которая синхронизируется в облако (Яндекс.Диск, GitHub). Используйте переменные окружения: добавьте export TG_TOKEN=... в ~/.zshenv и читайте в скрипте через $TG_TOKEN. Скрипт без секретов — безопасно синхронизировать куда угодно.

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

Ошибка 1: Относительный путь к скрипту в command

Написать "command": ".claude/hooks/log.sh" кажется логичным, но это почти всегда ошибка. Claude Code не гарантирует, что текущая директория при запуске хука совпадает с корнем проекта. Скрипт не найдётся, хук молча упадёт, вы не получите никакого предупреждения — просто логирование не работает.

Правильно: всегда абсолютный путь. Для личных хуков, которые используются в нескольких проектах — ~/.claude/hooks/log.sh с раскрытием через $HOME. Для хуков конкретного проекта — полный путь вида /Users/yourname/projects/myproject/.claude/hooks/log.sh.

Ошибка 2: Скрипт завис — агент встал

Если ваш скрипт что-то делает медленно (например, отправляет запрос к внешнему API без таймаута), агент терпеливо ждёт его завершения. При значении timeout: 10 через 10 секунд хук будет принудительно прерван. Но если timeout не выставлен — ожидание может быть очень долгим.

Значение timeout по умолчанию — 600 секунд, это очень долго. Для логирования выставляйте 5–10. Для локального логирования хватит 5 секунд. Для запросов к внешним сервисам — 10-15. И добавляйте таймауты к внешним вызовам внутри скрипта: curl --max-time 5, requests.get(..., timeout=5) в Python.

Ошибка 3: Скрипт не исполняемый

Создали файл, прописали путь в конфиге — хук молча не работает. Причина: файл без бита исполнения. Это классика для всех, кто работает в терминале нечасто. После создания любого скрипта-хука всегда выполняйте chmod +x путь/к/скрипту. Проверить: ls -l путь/к/скрипту — должна быть буква x в правах.

Ошибка 4: Попытка читать содержимое файла через tool_input для больших документов

tool_input для инструмента Write содержит полное содержимое нового файла. Для небольших документов это удобно — можно анализировать текст прямо в хуке. Но если агент записывает большой файл (отчёт на 500 страниц, выгрузка данных), tool_input становится огромным, скрипт тормозит, сессия деградирует.

Для хуков логирования используйте file_path из tool_input. Содержимое файла читайте с диска отдельно, если оно вам действительно нужно — и только после проверки размера файла.

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

Command hook на PostToolUse нужен, когда:

Вы запускаете агента на длинные пакетные задачи — обработать 50 отчётов, обновить 30 договоров, сгенерировать серию писем. В конце нужно понять, что именно было изменено и в каком порядке. Без хука у вас только результат, но не история.

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

Вы интегрируете агента в рабочий процесс с командой. Агент обновил отчёт — хук отправил уведомление в общий чат или записал в общий реестр изменений. Команда видит активность агента без необходимости самому рассказывать «что там агент делал».

Вы хотите статистику. За месяц агент изменил 400 файлов — а сколько договоров, сколько отчётов, сколько писем? Хук, который классифицирует файлы по папкам и типам, даёт эту статистику автоматически.

Command hook не нужен, когда:

Задача разовая и простая. Попросили агента подготовить одно письмо — проверили результат сами. Хук здесь добавляет сложность без пользы.

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

Производительность критична. Если агент делает сотни маленьких изменений подряд (например, массово обновляет метаданные), каждое из них запустит хук. 100 изменений × 0.5 секунды на хук = 50 секунд overhead. В таких сценариях хук лучше отключить на время массовых операций или поставить очень агрессивный таймаут с быстрым fallback.

Комбинация PreToolUse + PostToolUse

PreToolUse из дня 38 проверяет документ до записи. PostToolUse с command hook фиксирует факт записи после. Вместе они дают полный контроль: документ проходит проверку качества, и каждое сохранение документируется. Это разумная комбинация для рабочих документов, которые уходят внешним адресатам.

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

День 38 — PreToolUse и контроль до действия. Сегодняшний PostToolUse — обратная сторона той же медали. PreToolUse блокирует нежелательные действия до их выполнения, PostToolUse реагирует на действия после. Вместе они образуют систему контроля: входной фильтр + исходящий журнал. Если вы пропустили день 38, прочитайте его перед тем, как комбинировать оба типа хуков.

День 41 — система хуков: все типы событий. PostToolUse — один из нескольких типов. В дне 41 разобрана полная картина: когда что срабатывает, как события соотносятся друг с другом, в какой последовательности вызываются хуки при сложных сценариях. Если вы хотите строить сложные цепочки реакций — это фундамент.

День 46 — HTTP hooks. Command hook запускает локальный скрипт. HTTP hook отправляет запрос на внешний URL — в вашу корпоративную систему, в Notion API, в любой вебхук-эндпоинт. Если вы хотите, чтобы агент «сообщал» о своих действиях не в файл, а в внешний сервис, — следующий урок именно об этом. Логика та же, протокол другой.

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

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

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

  1. Создайте файл ~/.claude/hooks/log-changes.sh — используйте базовый пример из раздела «Шаг 3».
  2. Замените в скрипте имя вашего пользователя на реальное (или используйте переменную $HOME).
  3. Выполните chmod +x ~/.claude/hooks/log-changes.sh.
  4. Откройте ~/.claude/settings.json (или создайте его) и добавьте секцию hooks с PostToolUse по шаблону из урока.
  5. Запустите Claude Code и попросите создать любой тестовый файл: «Создай файл test-45.md с текстом "Проверка журнала"».
  6. Откройте ~/Logs/claude-changes.log и убедитесь, что там появилась строчка с именем файла.

Критерий «выполнено»: в файле ~/Logs/claude-changes.log есть хотя бы одна строчка, зафиксированная автоматически после того, как агент создал тестовый файл.

Резюме

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