раздел 06 · подстраница 4

Отладка hooks

Hooks ломаются молча. Hook не сработал, JSON не валиден, переменная пустая - Claude молча идёт дальше, и вы не сразу понимаете, что hook вообще не запускался. Здесь - чек-лист, как быстро найти причину.

Зачем

Самая частая жалоба: "написал hook, он не работает". В 90% случаев причина - в одной из четырёх типовых ошибок. Если знать, куда смотреть, диагностика занимает 2 минуты, а не час.

Куда смотреть в первую очередь

1. Логи Claude Code

ls -la ~/.claude/logs/
tail -f ~/.claude/logs/claude.log

Там видно, какие hooks триггерились, с каким exit-кодом, что писали в stderr.

2. Запустить Claude с --debug

claude --debug

В debug-режиме CLI печатает прямо в терминал каждый запуск hook с командой, env, exit-кодом и stderr.

[hook] PostToolUse matcher=Edit|Write
[hook] cmd: .claude/hooks/format.sh
[hook] env: CLAUDE_TOOL_NAME=Edit CLAUDE_FILE_PATHS=src/app.py
[hook] exit: 0
[hook] stderr: (empty)

3. Валидировать settings.json

cat ~/.claude/settings.json | python3 -m json.tool
# или
jq . ~/.claude/settings.json

Если выводит ошибку - JSON битый, hooks не работают вообще. Самая частая причина:

  • Лишняя запятая после последнего элемента массива/объекта.
  • Незакрытая скобка.
  • Кавычки внутри command без экранирования (\").

Топ-5 причин "hook не работает"

Причина 1. Битый JSON

Симптом: hooks не срабатывают вообще, никакие. Claude даже не пишет ошибку.

Решение:

jq . ~/.claude/settings.json
# покажет место ошибки

Причина 2. Не тот уровень файла

Симптом: hook есть в ~/.claude/settings.json, но в проекте не срабатывает.

Возможные причины:

  • В проекте есть .claude/settings.json который переопределяет структуру (для permissions).
  • Или вы добавили hook в settings.local.json, а он не подцепляется (опечатка в имени файла).

Решение: проверить, какие три файла существуют:

ls -la ~/.claude/settings.json .claude/settings.json .claude/settings.local.json 2>/dev/null

Причина 3. matcher не совпадает

Симптом: hook должен срабатывать на Edit, но не срабатывает.

Возможные причины:

  • Опечатка: "matcher": "edit" вместо "Edit" - регистр важен.
  • Используется MultiEdit или Write, а matcher только Edit.

Решение: расширьте matcher или используйте *:

"matcher": "Edit|Write|MultiEdit"

Причина 4. Hook падает с exit 1 без сообщения

Симптом: что-то блокируется, но непонятно почему.

Решение: всегда писать в stderr перед exit:

[[ "$CLAUDE_FILE_PATHS" == *".env"* ]] && {
  echo "blocked: tried to read .env file" >&2
  exit 1
}
exit 0

Тогда в --debug режиме сразу видно, что и почему.

Причина 5. Переменные окружения пустые

Симптом: $CLAUDE_FILE_PATHS пуст или $CLAUDE_TOOL_NAME undefined.

Возможные причины:

  • Hook повешен на событие, где этой переменной нет (например, $CLAUDE_FILE_PATHS только в Edit/Write, не в Bash).
  • В JSON используется $CLAUDE_FILE_PATHS в двойных кавычках без экранирования - shell не получит переменную.

Решение: дампьте env прямо из hook:

env | grep CLAUDE > /tmp/hook-env-debug.txt

И смотрите, что реально пришло.

Как тестировать hook без перезапуска Claude

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

# симулируем PostToolUse для Edit
export CLAUDE_TOOL_NAME=Edit
export CLAUDE_FILE_PATHS="src/app.py"
export CLAUDE_SESSION_ID="test"
export CLAUDE_PROJECT_DIR="$PWD"

bash .claude/hooks/format.sh
echo "exit code: $?"

Если в терминале отработало - в Claude отработает тоже.

Изоляция: где проблема, в hook или в Claude

Простой тест: положите явно работающий hook:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [{ "type": "command", "command": "echo 'hook fired' > /tmp/hook-test.log" }]
      }
    ]
  }
}

После следующего Stop в Claude:

cat /tmp/hook-test.log

Если файла нет - проблема в Claude/конфиге. Если есть - значит hooks вообще работают, дело в вашей конкретной команде.

Чек-лист при отладке

  • [ ] jq . settings.json - валидный JSON?
  • [ ] claude --debug - hook вообще триггерится?
  • [ ] Команда сама по себе работает в терминале?
  • [ ] matcher покрывает реальные имена инструментов?
  • [ ] Переменные доступны на этом событии?
  • [ ] Есть exit 0 в конце успешной ветки?
  • [ ] Сообщение в stderr при exit 1?

Антипаттерны

  • Молчаливый exit 1 - вы не поймёте причину блока.
  • Hook без set -e или с глотанием ошибок - тихо ломается на середине.
  • Отладка через "печатаю в stdout" - stdout уходит в Claude как ответ инструмента и иногда ломает поток. Пишите в stderr или в файл.
  • Долгие команды без timeout - в одном из 100 запусков hook зависнет, и весь Claude встанет.

Полезные ссылки