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

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

Matchers — точечная настройка триггеров

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

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

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

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

На прошлых уроках вы познакомились с hooks — механизмом, который перехватывает действия Claude и запускает ваш код до или после них. Это мощно. Но есть проблема, которая обнаруживается сразу, как только вы начинаете пользоваться hooks всерьёз: они срабатывают слишком часто.

Представьте: вы настроили hook, который логирует каждый вызов Bash-команды. Казалось бы, удобно — видно всё, что делает Claude. Но Claude за одну задачу может вызывать Bash десятки раз: проверить файл, посмотреть размер директории, выполнить поиск, запустить скрипт. Лог превращается в свалку. Если вы хотели видеть только опасные команды — например, с rm или chmod — то девяносто процентов записей просто шум.

Или другой сценарий. Вы директор, работаете с документами. Настроили hook, который форматирует файл после каждого редактирования — чтобы структура соответствовала внутреннему стандарту. Логично. Но Claude редактирует не только ваши документы, он иногда редактирует собственные конфиги, временные файлы, скрипты. Если hook срабатывает на всё подряд, он начинает ломать то, до чего ему не следовало дотрагиваться.

Matchers — это механизм фильтрации, который решает ровно эту задачу. Вы говорите системе: «этот hook должен срабатывать не на любое событие, а только на конкретное — когда вызывается такой-то инструмент, или когда в команде есть такое-то слово, или когда редактируется файл с таким расширением». Всё остальное проходит мимо. Результат — hooks работают точечно, предсказуемо и без побочных эффектов.


Что это такое

Чтобы понять matchers, удобно использовать аналогию с фильтрами в почте. Представьте, что в вашем почтовом клиенте есть правило: «если письмо от адреса @застройщик.ru — переложить в папку Клиенты». Это и есть matcher: условие, которое проверяется перед тем, как правило применяется. Без условия правило бы применялось ко всем письмам подряд.

В Claude Code matcher — это поле в конфигурации hook, которое описывает, при каком условии hook должен активироваться. Условие проверяется по одному полю: на инструментальных событиях это имя инструмента (Bash, Edit, Read и так далее). Фильтровать по тексту команды или пути к файлу нужно полем if или внутри скрипта.

Ключевое различие

Событие hook (например, PreToolUse) определяет, в какой момент срабатывает проверка — до или после вызова инструмента. Matcher определяет, для каких именно вызовов. Событие — это «когда», matcher — это «для чего именно».

Matcher может быть трёх видов. Первый — точная строка: "Bash" — hook срабатывает только когда Claude вызывает инструмент Bash. Второй — несколько значений через пайп или запятую: "Bash|Edit", "Edit, Write". — срабатывает на любой из перечисленных инструментов. Третий вид — регулярное выражение: как только в матчере появляется любой другой символ, он трактуется как JS-регулярка без якорей, поэтому Edit.* поймает ещё и NotebookEdit. Третий — регулярное выражение: позволяет задавать более сложные условия, например, «команда содержит rm» или «файл заканчивается на .md».

Дополнительно существует поле if — оно позволяет задать условие прямо внутри конфигурации hook, без отдельного скрипта-проверки. Это удобно для простых случаев.


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

Разберём конкретные примеры из жизни директора, который работает с документами, отчётами и письмами.

Базовая структура hook с matcher

Конфигурация hooks живёт в файле .claude/settings.json в рабочей папке. Вот как выглядит типичная запись:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Bash вызван' >> ~/logs/claude-bash.log"
          }
        ]
      }
    ]
  }
}

Здесь "matcher": "Bash" означает: этот hook активируется только когда Claude собирается вызвать инструмент Bash. Если Claude вызывает Edit или Read — hook молчит.

Пример 1: Предупреждение перед опасными командами

Задача: вы хотите, чтобы Claude останавливался и предупреждал вас перед любой командой, которая содержит rm — удаление файлов. Это защита от случайного удаления важных документов.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command // \"\"' | grep -q 'rm ' && echo 'ВНИМАНИЕ: обнаружена команда удаления' >&2 && exit 2 || exit 0"
          }
        ]
      }
    ]
  }
}

Что здесь происходит:
- "matcher": "Bash" — hook смотрит только на Bash-вызовы, не трогает редактирование файлов
- Скрипт читает событие JSON-ом со стандартного входа и достаёт команду из .tool_input.command
- grep -q 'rm ' ищет слово rm в команде
- Если нашёл — выводит предупреждение в stderr и возвращает код exit 2 (это блокирует выполнение, а текст из stderr уходит Claude как причина отказа)
- Если не нашёл — exit 0, всё нормально, продолжаем

exit 2 vs exit 1

В hooks Claude Code коды выхода имеют значение. exit 0 — всё в порядке, продолжить. exit 1 — ошибка, но выполнить. exit 2 — заблокировать выполнение; текст из stderr уходит Claude как причина отказа. Если нужно не запретить, а спросить пользователя — верните JSON с hookSpecificOutput.permissionDecision: "ask" и кодом выхода 0.

Пример 2: Hook только для файлов .md

Задача: вы ведёте базу знаний в markdown-файлах. Хотите, чтобы после каждого редактирования .md-файла автоматически обновлялся индекс — список всех документов с датой изменения.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 -c \"import sys,json; p=json.load(sys.stdin).get('tool_input',{}).get('file_path',''); sys.exit(0 if p.endswith('.md') else 1)\" && python3 ~/scripts/update-index.py"
          }
        ]
      }
    ]
  }
}

Здесь matcher "Edit" отбирает только события редактирования. Внутри скрипта дополнительная проверка: если путь не заканчивается на .md — выходим с кодом 1 (пропускаем), иначе запускаем update-index.py.

Для более читаемой конфигурации ту же логику можно вынести в отдельный скрипт:

#!/bin/bash
# ~/scripts/check-and-index.sh
INPUT=$(cat)
PATH_VAL=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))")

if [[ "$PATH_VAL" == *.md ]]; then
  python3 ~/scripts/update-index.py "$PATH_VAL"
fi

И hook становится чище:

{
  "matcher": "Edit",
  "hooks": [
    {
      "type": "command",
      "command": "~/scripts/check-and-index.sh"
    }
  ]
}

Пример 3: Несколько инструментов через пайп

Задача: вы хотите логировать все изменения файлов — и через Edit (прямое редактирование), и через Bash (например, когда Claude пишет файл командой echo ... > file.txt).

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '\"\\(now|strftime(\"%Y-%m-%d %H:%M:%S\")) | \\(.tool_name) | \\(.tool_input.file_path // \"-\")\"' >> ~/logs/file-changes.log"
          }
        ]
      }
    ]
  }
}

Запись "matcher": "Bash|Edit" означает: активировать hook если инструмент — Bash ИЛИ Edit. Через пайп можно перечислить любое количество инструментов.

Пример 4: Matcher на событие UserPromptSubmit

Это отдельный тип события — оно срабатывает когда вы отправляете сообщение Claude, ещё до того как он начал работать. Здесь matcher работает по тексту запроса.

Задача: когда вы пишете слово «срочно» в запросе, Claude должен немедленно уведомить вас уведомлением на Mac (через osascript), чтобы вы не отвлеклись и не пропустили, что задача принята.

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "срочно",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude принял срочную задачу\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Осторожно: у UserPromptSubmit matcher не поддерживается — поле молча игнорируется, и хук сработает на каждый ваш запрос. Фильтровать по тексту нужно внутри скрипта, читая поле prompt из JSON на stdin. Фильтровать по тексту запроса нужно внутри скрипта, читая поле prompt из JSON на стандартном входе.

Регистр в матчерах

По умолчанию строковый matcher чувствителен к регистру. «Срочно» и «срочно» — разные строки. Если вам нужен регистронезависимый поиск, регистронезависимого режима у матчера нет: он проверяется движком JavaScript RegExp, а inline-флаг (?i) тот не поддерживает. Перечислите варианты явно (Срочно|срочно) или фильтруйте регистр внутри скрипта.

Поле if: условие прямо в конфиге

Поле if — это альтернативный способ задать условие, без написания скрипта. Оно принимает одно правило в синтаксисе прав доступа — то же, что в permissions.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "if": "Bash(sudo *)",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Попытка использовать sudo!' >> ~/logs/security.log"
          }
        ]
      }
    ]
  }
}

Поле if принимает одно правило в синтаксисе прав доступаBash(git *), Edit(*.ts), Edit(**/*.md). Никакого мини-языка с функциями и операторами &&/|| нет: на каждое условие заводят отдельный обработчик. Работает if только на инструментальных событиях. Это удобно для простых условий — не нужно писать отдельный bash-скрипт только для одной проверки.


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

Ошибка 1: matcher написан с опечаткой или в неправильном регистре

"matcher": "bash"

Это не сработает. Названия инструментов пишутся с заглавной буквы: "Bash", "Edit", "Read", "WebFetch". Маленькие буквы — ни одно событие не совпадёт, hook никогда не активируется. Самое неприятное — никакой ошибки не будет, просто тишина.

Решение: всегда проверяйте точное написание инструментов. Список основных: Bash, PowerShell, Edit, Write, Read, Glob, Grep, Agent, WebFetch, WebSearch, mcp__* (для MCP-инструментов).

Ошибка 2: hook срабатывает, но не на те файлы

Вы написали "matcher": "Edit" и ожидали, что hook будет работать только с .md-файлами. Но он срабатывает на любой Edit — и на .json, и на .py, и на временные файлы Claude.

Причина: matcher "Edit" проверяет только имя инструмента, не содержимое вызова. Чтобы фильтровать по пути файла, нужна дополнительная проверка внутри скрипта — или поле if.

Решение:

{
  "matcher": "Edit",
  "if": "Edit(**/*.md)",
  "hooks": [...]
}

Или внутри скрипта: читаете JSON со стандартного входа, извлекаете tool_input.file_path, проверяете расширение.

Ошибка 3: Слишком широкий matcher через regexp ловит лишнее

Допустим, вы хотите поймать команды с rm и написали паттерн "matcher": ".*rm.*". Это регулярное выражение совпадает с любой строкой, содержащей rm где угодно — в том числе chmod, rm в имени переменной, from в Python-импорте.

Результат: hook срабатывает слишком часто, блокирует невинные команды, Claude жалуется на постоянные остановки.

Решение: будьте точнее в регулярных выражениях. Для команды удаления лучше искать \brm\b (слово целиком) или (^|\s)rm\s (пробелы вокруг):

"matcher": "Bash",
"if": "Bash(rm *)"

Тестируйте regexp перед деплоем

Регулярные выражения в matchers проверить заранее сложно — нет встроенного тестового режима. Пишите паттерны максимально конкретно, начинайте с точных строк, добавляйте сложность по мере необходимости.

Ошибка 4: Забыть про MCP-инструменты

Если вы используете MCP-серверы (день 51+), их инструменты называются иначе: mcp__имя_сервера__имя_инструмента. Matcher "Bash" на них не реагирует. Если хотите перехватывать MCP-вызовы — нужен отдельный matcher:

"matcher": "mcp__clients__phone_lookup"

Или через regexp для всех MCP-инструментов одного сервера:

"matcher": "mcp__clients__.*"

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

Используйте matchers, если:

Не усложняйте с matchers, если:

Порядок отладки

Если не понимаете, почему hook не срабатывает — временно уберите matcher полностью. Если hook заработал — значит matcher не совпадает с реальными событиями. Добавьте логирование всех вызовов и посмотрите точные названия инструментов в логе. Потом верните matcher с правильным значением.


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

День 41 (Hooks: события) — там мы разобрали, какие события существуют: PreToolUse, PostToolUse, UserPromptSubmit, Stop. Matchers — это следующий уровень: не просто выбрать событие, но и уточнить, для каких конкретно вызовов внутри этого события hook должен работать.

День 44 (Exit codes и управление потоком) — коды выхода exit 0, exit 1, exit 2 определяют, что произойдёт после срабатывания hook. Matchers решают, сработает ли hook вообще. Вместе они дают полный контроль: «для такого-то инструмента с такой-то командой — заблокировать и спросить».

День 48 (Аудит и нотификации через hooks) — там мы соберём практическую систему аудита действий Claude. Matchers там будут использоваться активно: логировать не всё подряд, а только значимые события — редактирование важных файлов, опасные команды, внешние запросы.


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

Откройте файл .claude/settings.json в любой вашей рабочей папке (если его нет — создайте с базовой структурой {}). Добавьте один hook с matcher:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '\"\\(now|strftime(\"%H:%M:%S\")) BASH: \\(.tool_input.command // \"-\")\"' >> /tmp/claude-bash-audit.log"
          }
        ]
      }
    ]
  }
}

Затем запустите claude в этой папке и попросите что-нибудь, что вызовет Bash — например: «посмотри, сколько файлов в этой папке». После завершения откройте лог:

cat /tmp/claude-bash-audit.log

Критерий выполнения: в логе есть строки только с Bash-командами, без записей о чтении файлов или других операциях. Matcher работает.


Резюме

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