Прямой ответ
Git-хуки — скрипты, которые git запускает на определённых событиях; практически важны два: pre-commit (проверки до создания коммита) и commit-msg (проверка сообщения). Вручную писать их не нужно — стандартом стал фреймворк pre-commit: один YAML-конфиг в репозитории, pre-commit install у каждого разработчика — и линтеры, форматтеры, проверка секретов и десяток гигиенических проверок работают одинаково у всей команды и в CI. Правило баланса: на коммите — только быстрое (секунды), тяжёлое (тесты, сборка) остаётся в CI; хук, который идёт минуту, разработчики начнут обходить через --no-verify в первую же неделю.
Быстрый старт: конфиг и установка
.pre-commit-config.yaml:
repos:
- repo:
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-added-large-files
- id: check-merge-conflict
- repo:
rev: v0.6.x
hooks:
- id: ruff
- id: ruff-format
- repo:
rev: v8.x
hooks:
- id: gitleaks
pip install pre-commit && pre-commit install — и хуки активны. Механика: при git commit pre-commit прогоняет хуки только по файлам в индексе (staged) — отсюда скорость; падающий хук блокирует коммит, форматтеры правят файлы на месте (правка попадает в следующий коммит — или добавьте в индекс и повторите). Команда pre-commit run --all-files прогоняет всё вручную — её же зовёт CI, и дублируется она целью в таск-раннере (паттерн из статьи про Makefile/just).
Что обязательно, что полезно, что избыточно
Обязательный минимум: gitleaks (секреты в коммите — невосстановимая утечка; порядок действий описан в статье про секреты), форматтер (ruff-format/black — заканчивает споры о стиле навсегда), проверка больших файлов и merge-конфликтных маркеров. Полезно по стеку: линтер (ruff/eslint), проверка валидности конфигов (yamllint, check-toml, check-json), mypy на изменённые файлы, проверка миграций. Избыточно на коммите: тесты, сборка, тяжёлые статические анализаторы — это CI; хуки и CI не конкуренты, а эшелоны: хук ловит мгновенно и дёшево, CI — полно и авторитетно (про свой раннер — статья про self-hosted CI).
Commit-msg и конвенция сообщений
Хук commit-msg проверяет формат сообщения — обычно Conventional Commits (feat:, fix:, chore: — через commitizen/conventional-pre-commit). Смысл не в эстетике: машиночитаемые сообщения дают автогенерацию CHANGELOG и семантическое версионирование. Для команд, которым это не нужно, достаточно проверки «первая строка ≤ 72 символа, не заканчивается точкой» — дисциплина истории репозитория без бюрократии.
Обходы, и почему они допустимы
git commit --no-verify пропускает хуки — и это правильный аварийный клапан: иногда нужно срочно закоммитить WIP, не дожидаясь линтера. Проблема не в обходе, а в его незаметности: CI обязан прогонять тот же pre-commit по всему дереву — тогда обход локально не означает пропуска проверки вообще. Вторая частая история — «хуки не установились у нового разработчика»: лечится строкой pre-commit install в make install/документе онбординга и проверкой в CI. Третий нюанс: версии хуков фиксируются полем rev — обновляйте их осознанно (pre-commit autoupdate раз в месяц), а не «когда сломается».
Производительность: хуки не должны бесить
Бюджет pre-commit-прогона — 2–5 секунд на типичном коммите. Что его ломает: mypy/pyright по всему проекту вместо изменённых файлов; хуки, запускающие контейнеры (тяжёлые интеграционные проверки — в CI); кэш pre-commit в холодном состоянии (первый прогон качает окружения — предупредите команду). Если хук хронически тормозит — переносите его на стадию push (pre-push) или в CI: pre-commit поддерживает стадии, и это штатный механизм, а не костыль.
Локальные хуки против серверных: полная картина
Хуки pre-commit — клиентская сторона, и её принципиальное ограничение: недобросовестный или неосведомлённый разработчик обходит их одной опцией. Поэтому серьёзные проверки дублируются серверно. Первый серверный эшелон — CI на каждый push (тот же pre-commit в режиме run --all-files, тесты, сборка). Второй, для защищённых веток, — branch protection: merge невозможен без зелёного CI. Третий, в зрелых организациях, — серверные git-хуки на стороне хостинга (reject-force-push, проверка подписанных коммитов). Пирамида получается простая: локальные хуки для скорости и удобства, серверные — для гарантий; ни один слой не заменяет другой, но каждый уменьшает нагрузку на следующий.
Отдельный сюжет — миграция legacy-репозитория, где хуков не было никогда. Порядок безболезненный: сначала подключите только некритичные проверки с автофиксом (whitespace, конец файла) — команда привыкает к механике; через неделю добавьте форматтер на всё дерево одним «формат-коммитом» (предупредив коллег не держать открытых веток в этот момент); затем линтер в режиме «предупреждает, но не блокирует», и только когда красных сообщений почти не осталось — в блокирующем. Внедрение «всё и сразу» в старом репозитории гарантирует неделю разбора завалов и стойкое раздражение команды против инструмента.
FAQ
Хуки живут в репозитории или у разработчика?
Конфиг — в репозитории (.pre-commit-config.yaml), установка — локальная (pre-commit install пишет в .git/hooks). Клонирующий репозиторий автоматически хуки не получает — это осознанное ограничение безопасности git.
Можно ли без Python-проекта использовать pre-commit? Да: фреймворк языково-независим, есть хуки для Node, Go, Rust, общие проверки файлов; ставится как отдельный бинарник.
Почему форматтер «съел» мои правки при коммите?
Не съел — поправил файлы и оставил их не-индексированными: добавьте изменённое (git add -u) и повторите коммит. Это типовое недопонимание первой недели.
Нужны ли хуки, если есть CI? Да: они дают обратную связь за секунды локально вместо минут в очереди CI и разгружают раннеры от заведомо красных коммитов. Эшелоны дополняют друг друга.
Что делать, если хук сломался и блокирует срочный коммит?
Аварийный путь — git commit --no-verify, штатный — SKIP=gitleaks pre-commit run (пропуск конкретного хука) или откат rev проблемного хука. После срочного коммита причину стоит разобрать: хуки ломаются не сами, обычно это обновление версии или окружения.
Нужны ли хуки в монорепозитории?
Да, с фильтрами по путям: pre-commit умеет files:/exclude:, и хук линтера гоняется только по файлам своего пакета. Без фильтров прогон по всему дереву на каждом коммите убьёт и скорость, и мотивацию команды.
Источники: pre-commit.com, Pro Git (Git Hooks), gitleaks (ссылки в шапке).