Суть урока
В предыдущем уроке (Hooks: автоматические правила) — теория: около 30 событий, 5 обработчиков, JSON-протокол. Здесь — три реальных хука, которые используются каждый день. Первый защищает от случайной утечки API-ключей. Второй не даёт превысить бюджет. Третий пишет полный журнал что Claude трогал и когда. Бонус — HTTP webhook для внешних уведомлений. Строим с нуля, разбираем каждую строку.
Ключевые концепции
- pre-tool-use-no-secrets.sh — сканирует файлы перед записью на наличие паттернов секретов
- pre-tool-use-budget-check.sh — проверяет счётчик операций и останавливает если лимит превышен
- post-tool-use-audit-log.sh — записывает лог каждого изменения файла с timestamp
- exit code 0 / 2 — как хук сообщает Claude Code "разрешить" (0) или "заблокировать" (2)
- matcher — фильтр: на какие инструменты реагировать (
"Write|Edit","Bash","*") - tool_input — JSON-объект с входными данными инструмента (путь, содержимое, команда)
- grep — поиск паттернов (API-ключи, токены) в содержимом файлов
- jq — парсинг JSON который Claude Code передаёт хуку через stdin
- Тестирование хука — как проверить что хук срабатывает правильно
Теория
Как хук работает технически
Claude Code вызывает хук как обычный bash-скрипт. Передаёт данные через stdin в формате JSON. Хук анализирует данные, выполняет свою логику, возвращает результат через exit code.
Claude Code хочет записать файл
↓
Вызывает PreToolUse хук (matcher: "Write|Edit")
↓
Передаёт через stdin JSON:
{
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/project/config.py",
"content": "API_KEY = 'sk-proj-abc123...'"
},
"session_id": "abc123",
"cwd": "/Users/me/project"
}
↓
Хук анализирует tool_input.content
↓
exit 0 → Claude Code записывает файл
exit 2 → Claude Code останавливается, stderr хука передаётся ClaudeВажно: блокирует exit code 2, не 1. Exit code 1 — это обычная ошибка скрипта, Claude продолжит работу.
При блокировке хук пишет сообщение в stderr (>&2) — это то что увидит Claude и сообщит пользователю.
Хук 1: pre-tool-use-no-secrets.sh
Задача: предотвратить случайный хардкод API-ключей, токенов и паролей в коде.
Проблема которую решает: разработчик часто вставляет ключ прямо в код "на время", забывает удалить, коммитит в git. Хук останавливает это до записи файла.
Создание файла
mkdir -p ~/.claude/hooks
touch ~/.claude/hooks/pre-tool-use-no-secrets.sh
chmod +x ~/.claude/hooks/pre-tool-use-no-secrets.shСодержимое скрипта
#!/bin/bash
# pre-tool-use-no-secrets.sh
# Блокирует запись файлов с захардкоженными секретами
# Читаем данные от Claude Code через stdin
INPUT=$(cat)
# Извлекаем данные из официального JSON-формата
# tool_name — на верхнем уровне
# file_path и content — внутри tool_input
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
# Для Edit-инструмента контент в поле new_string
if [[ "$TOOL_NAME" == "Edit" ]]; then
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty')
fi
# Проверяем только инструменты записи файлов
# (matcher "Write|Edit" в settings.json уже фильтрует,
# но двойная проверка не помешает)
if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" ]]; then
exit 0 # Не запись — пропускаем
fi
# Паттерны которые ищем (возможные API-ключи и токены)
PATTERNS=(
'sk-[a-zA-Z0-9]{20,}' # OpenAI / Anthropic API keys
'ghp_[a-zA-Z0-9]{36}' # GitHub Personal Access Token
'xoxb-[0-9]+-[a-zA-Z0-9]+' # Slack Bot Token
'AKIA[0-9A-Z]{16}' # AWS Access Key
'AIza[0-9A-Za-z_-]{35}' # Google API Key
'password\s*=\s*["\'][^"\']+["\']' # Явный пароль в коде
'secret\s*=\s*["\'][^"\']+["\']' # Явный секрет в коде
)
# Проверяем контент на каждый паттерн
for PATTERN in "${PATTERNS[@]}"; do
if echo "$CONTENT" | grep -qE "$PATTERN"; then
# Сообщение в stderr — его увидит Claude и передаст пользователю
echo "БЛОКИРОВАНО: Обнаружен возможный секрет/API-ключ в файле $FILE_PATH" >&2
echo "Паттерн: $PATTERN" >&2
echo "Используй переменные окружения (.env) или secrets manager вместо хардкода." >&2
exit 2 # Exit code 2 = блокировка действия
fi
done
exit 0 # Секретов не найдено — разрешаемДобавление в settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-tool-use-no-secrets.sh",
"timeout": 30
}
]
}
]
}
}Обрати внимание на структуру:
"PreToolUse"— CamelCase (неpre_tool_use)"matcher": "Write|Edit"— хук срабатывает только при записи/редактировании файлов (не при чтении, не при bash-командах)"type": "command"(не"type": "bash")"timeout": 30— если скрипт не ответит за 30 секунд, Claude продолжит
Тест хука
Дай Claude Code команду:
Создай файл config.py с содержимым: API_KEY = 'sk-proj-test123456789012345678901234'
Ожидаемый результат:
БЛОКИРОВАНО: Обнаружен возможный секрет/API-ключ в файле config.py Используй переменные окружения (.env) или secrets manager вместо хардкода.
Claude Code не запишет файл. Предложит использовать .env.
Хук 2: pre-tool-use-budget-check.sh
Задача: контролировать расходы — останавливать Claude Code если операций за день слишком много.
Проблема которую решает: длинные автономные задачи могут делать тысячи операций. Хук устанавливает жёсткий лимит.
#!/bin/bash
# pre-tool-use-budget-check.sh
# Контроль бюджета по количеству операций в сутки
COUNTER_FILE="/tmp/claude_ops_$(date +%Y%m%d).count"
DAILY_LIMIT=500 # Максимум операций за день
# Читаем текущий счётчик
if [ -f "$COUNTER_FILE" ]; then
CURRENT=$(cat "$COUNTER_FILE")
else
CURRENT=0
fi
# Проверяем лимит
if [ "$CURRENT" -ge "$DAILY_LIMIT" ]; then
echo "СТОП: Дневной лимит операций достигнут ($CURRENT/$DAILY_LIMIT)" >&2
echo "Сброс произойдёт в полночь. Для ручного сброса: rm $COUNTER_FILE" >&2
exit 2 # Exit code 2 = блокировка
fi
# Увеличиваем счётчик
echo $((CURRENT + 1)) > "$COUNTER_FILE"
# Предупреждение на 80% использования (через stdout — не блокирует)
THRESHOLD=$((DAILY_LIMIT * 80 / 100))
if [ "$CURRENT" -ge "$THRESHOLD" ]; then
echo "ПРЕДУПРЕЖДЕНИЕ: Использовано $CURRENT/$DAILY_LIMIT операций (80% лимита)"
fi
exit 0Добавление в settings.json (рядом с первым хуком)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-tool-use-no-secrets.sh",
"timeout": 30
}
]
},
{
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-tool-use-budget-check.sh"
}
]
}
]
}
}Обрати внимание: у первого хука "matcher": "Write|Edit" — он проверяет только запись файлов. У второго matcher не указан — значит он срабатывает на все инструменты.
Несколько PreToolUse хуков выполняются параллельно. Если любой возвращает exit 2 — действие блокируется.
Хук 3: post-tool-use-audit-log.sh
Задача: вести полный журнал что Claude изменял — какие файлы, в какое время, какой инструмент.
Проблема которую решает: после сессии непонятно что именно изменил Claude. Лог позволяет отследить каждое изменение и откатить если нужно.
#!/bin/bash
# post-tool-use-audit-log.sh
# Аудит-лог всех изменений файлов
LOG_FILE="$HOME/.claude/audit-log.txt"
mkdir -p "$(dirname "$LOG_FILE")"
# Читаем данные от Claude Code через stdin
INPUT=$(cat)
# Извлекаем информацию о действии
# tool_name — верхний уровень, остальное — внутри tool_input
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
PROJECT=$(basename "$(pwd)")
# Пишем только действия с файлами
if [[ -n "$FILE_PATH" ]]; then
echo "[$TIMESTAMP] PROJECT=$PROJECT TOOL=$TOOL_NAME FILE=$FILE_PATH" >> "$LOG_FILE"
fi
# Также логируем bash-команды (поле command внутри tool_input)
BASH_CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if [[ -n "$BASH_CMD" ]]; then
# Показываем первые 100 символов команды
SHORT_CMD="${BASH_CMD:0:100}"
echo "[$TIMESTAMP] PROJECT=$PROJECT BASH: $SHORT_CMD" >> "$LOG_FILE"
fi
exit 0 # PostToolUse хуки не блокируют — всегда exit 0Полный settings.json с тремя хуками + уведомление
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-tool-use-no-secrets.sh",
"timeout": 30
}
]
},
{
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-tool-use-budget-check.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit|Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/post-tool-use-audit-log.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude завершил задачу\" with title \"Claude Code\"'"
}
]
}
]
}
}Чеклист формата — проверь у себя:
- Имена событий CamelCase:
PreToolUse,PostToolUse,Stop(не snake_case) - Тип обработчика:
"type": "command"(не"type": "bash") - Каждое событие → массив → объект с
matcher+hooks→ массив обработчиков matcherфильтрует инструменты:"Write|Edit","Bash", или пусто для всех
Как читать аудит-лог
[2026-10-04 14:23:01] PROJECT=acme-realty TOOL=Write FILE=/project/index.md
[2026-10-04 14:23:04] PROJECT=acme-realty TOOL=Edit FILE=/project/CLAUDE.md
[2026-10-04 14:23:09] PROJECT=acme-realty BASH: mkdir -p .claude/skills
[2026-10-04 14:25:33] PROJECT=my-platform TOOL=Write FILE=/strategy/plan.mdВидишь: время, проект, инструмент, файл. Если что-то сломалось — знаешь точно что и когда Claude трогал.
# Посмотреть лог за сегодня
tail -50 ~/.claude/audit-log.txt
# Найти все изменения конкретного файла
grep "CLAUDE.md" ~/.claude/audit-log.txt
# Найти все действия в конкретном проекте
grep "PROJECT=acme-realty" ~/.claude/audit-log.txtИз практики: реальный кейс с .env файлом
Из транскрипта: "Идеально, мы бы не хотели чтобы Claude затронул .env документ, потому что если он его изменяет — все автоматизации ломаются, они все зависят от этих паролей."
Вариация хука для защиты конкретного файла:
#!/bin/bash
# Защита .env файла от любых изменений Claude
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Блокируем любые изменения .env
if [[ "$FILE_PATH" == *".env"* ]] && [[ "$TOOL_NAME" == "Write" || "$TOOL_NAME" == "Edit" ]]; then
echo "БЛОКИРОВАНО: .env файл защищён от изменений" >&2
echo "Файл содержит секреты. Редактируй вручную." >&2
exit 2 # Exit code 2 = блокировка
fi
exit 0После этого хука Claude Code буквально ответит: "Я не могу этого сделать — хук блокирует мне доступ к этому файлу."
Ещё проще — можно использовать "if" фильтр в settings.json вместо проверки в скрипте:
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"if": "Write(*.env)",
"command": "echo 'БЛОКИРОВАНО: .env защищён' >&2 && exit 2"
},
{
"type": "command",
"if": "Edit(*.env)",
"command": "echo 'БЛОКИРОВАНО: .env защищён' >&2 && exit 2"
}
]
}Здесь "if" работает как дополнительный фильтр по аргументам: форма Инструмент(шаблон) проверяет один инструмент, поэтому для Write и Edit два обработчика. Хук сработает только для файлов .env.
Бонус: HTTP webhook — хук без bash-скрипта
Не обязательно всё делать через bash. Если у тебя есть сервер (или сервис типа Telegram Bot API), можно отправлять данные через HTTP.
Пример: уведомление в Telegram при изменении файлов
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "http",
"url": "http://localhost:3000/hooks/file-changed",
"timeout": 10
}
]
}
]
}
}Claude Code отправит POST-запрос с JSON-данными о файле. Твой сервер получит:
{
"hook_event_name": "PostToolUse",
"tool_name": "Write",
"tool_input": { "file_path": "/project/index.md", "content": "..." },
"tool_response": "File written successfully",
"cwd": "/Users/me/project"
}Сервер может переслать это в Telegram, Slack, записать в базу — что угодно.
Когда использовать HTTP вместо command:
- Уведомления во внешний сервис (Telegram, Slack, Discord)
- Централизованный аудит для нескольких машин
- Когда логика проверки живёт на сервере (микросервис валидации)
Тестирование хуков: чеклист
После создания каждого хука — проверить:
Для no-secrets хука:
Создай файл test.py с содержимым: token = 'sk-proj-realkey123456789012345'
Ожидание: Claude заблокирован, видишь сообщение хука.
Для audit-log хука:
Создай файл test-audit.md с текстом "Тест аудита"
Затем: tail -5 ~/.claude/audit-log.txt — новая запись должна появиться.
Для stop-notification хука:
Что такое Claude Code? (короткий вопрос)
Ожидание: после ответа появится Mac OS уведомление.
Практика
Задание: Поднять все три хука
- Создай папку
~/.claude/hooks/ - Создай три bash-скрипта с содержимым из урока
- Дай права на выполнение:
chmod +x ~/.claude/hooks/*.sh - Создай или обнови
.claude/settings.json— добавь все три хука по шаблону из урока - Протестируй каждый хук (чеклист выше)
- Посмотри как выглядит аудит-лог после нескольких операций
Цель: три рабочих хука, понимание exit code логики, первый аудит-лог с реальными записями
Инструменты и ресурсы
jq— парсинг JSON в bash (brew install jqна Mac)chmod +x— права на выполнение скриптаosascript— Mac OS нативные уведомления (встроен в macOS)tail -f ~/.claude/audit-log.txt— live-просмотр лога в реальном времени/hooks— команда для просмотра активных хуков через Claude Code terminal
Ключевые выводы
exit 0 = разрешить, exit 2 = заблокировать. Не 1, а именно 2! Exit 1 — это просто ошибка скрипта, Claude продолжит работу.
При блокировке сообщение пишем в stderr (
>&2), не в stdout. Stderr передаётся Claude как причина блокировки.
matcherфильтрует по инструменту:"Write|Edit"— только файловые операции. Без matcher хук срабатывает на всё.
PostToolUse хуки всегда exit 0 — они логируют, не блокируют. Не нужно останавливать Claude после действия.
Данные от Claude Code приходят в формате JSON через stdin. Путь к файлу — в
tool_input.file_path, не простоfile_path.
Три хука закрывают три основные задачи: безопасность (секреты), экономика (бюджет), аудит (кто что трогал). Плюс HTTP-webhook для внешних уведомлений.
Формат settings.json: имена событий CamelCase (
PreToolUse), тип обработчика"command"(не"bash"), трёхуровневая вложенность.
Что дальше
Отметка хранится только в этом браузере и никуда не отправляется. Мой прогресс