01.08.2027 273 материалов

Фильтр AI-атрибуции в git-хуках: как точное регулярное выражение оказалось точнее, чем нужно

Разработчик обнаружил, что фильтр AI-атрибуции в git-хуках удаляет не только метки вроде «Co-Authored-By: Claude», но и любые коммиты, в которых техническое название инструмента Claude Code встречается как обычное существительное. Ошибка тоньше предыдущей и проще в объяснении.

Фильтр AI-атрибуции в git-хуках: как точное регулярное выражение оказалось точнее, чем нужно

Регулярное выражение с точным совпадением «claude code» не различает фразу «Generated with Claude Code» и предложение «fix: handle claude code MCP timeout» — оба проходят через один и тот же фильтр, и оба удаляются. Исправление — одна строка, но путь к ней потребовал пересмотра всей логики фильтра.

Предыстория: зачем вообще нужен фильтр атрибуции

В репозиторе, о котором идёт речь, настроен git-хук, который автоматически генерирует сообщения коммитов с помощью CLI-обёртки над Claude (claude -p). Это, по сути, аналог того, что делают Copilot, Cursor и другие AI-ассистенты для разработчиков: смотрят на diff и предлагают однострочное описание изменений.

Проблема в том, что проект придерживается правила «без AI-атрибуции в коммитах». Claude при генерации сообщений по умолчанию добавляет подпись вроде «Co-Authored-By: Claude» или футер «🤖 Generated with Claude Code». Эти метки нужно вырезать, и для этого в коде хука живёт список регулярных выражений _STRIP_PATTERNS.

Неделю назад автор уже исправлял один баг: фильтр использовал простой поиск подстроки, из-за чего коммит вида fix: retry llm calls on 429 with backoff молча удалялся — строка содержала «llm», и фильтр считал это маркером AI-генерации. Замена на регулярные выражения с границами слова (\b) казалась решением. Но «казалось» — ключевое слово.

Где именно сломалось

Текущий набор фильтров выглядит так:

_STRIP_PATTERNS = [
    r"co-authored-by\s*:",
    r"generated (with|by)\s+claude",
    r"\bclaude code\b",
    r"\bwritten by (an )?(ai|llm|claude|chatgpt|copilot)\b",
    r"\bai-generated\b",
    r"🤖",
]

Паттерн r"\bclaude code\b" нацелен на футер атрибуции вида «Generated with Claude Code». Но он не требует, чтобы перед словами «claude code» стояло что-то конкретное — ни предлога «with» или «by», ни глагола «generated». Достаточно просто упоминания инструмента по имени.

А в этом репозитории «Claude Code» — это собственное существительное. Так называется CLI-утилита, вокруг которой построены хуки, скрипты и MCP-сервер. Значительная часть коммитов посвящена именно работе с этим инструментом: исправление таймаутов, добавление документации, настройка воркфлоу для pull request'ов.

Тест: четыре из пяти коммитов уничтожены

Автор проверил фильтр на пяти реалистичных сообщениях коммитов:

  • fix: retry llm calls on 429 with backoffпроходит (проблема с «llm» уже исправлена).
  • docs: add claude code hook install instructionsудалён.
  • feat: wire up claude code review workflow for PRsудалён.
  • chore: document claude code slash commands in READMEудалён.
  • fix: handle claude code MCP timeout in server.pyудалён.

Ни один из четырёх удалённых коммитов не содержит AI-атрибуции. Все они — обычные технические описания работы с инструментом.

Почему это хуже предыдущего бага

Системный промпт генератора требует от Claude выдать ровно одну строку — без объяснений, без markdown-разметки. Фильтр работает построчно: если строка совпала с паттерном, она удаляется. Поскольку сообщение всегда однострочное, удаление этой строки означает полную потерю текста.

Хук prepare-commit-msg проверяет, не пустое ли сообщение. Пустая строка для него неотличима от ситуации «AI не сработал» или «хук не установлен». Это уже третий механизм, порождающий один и тот же скрытый симптом: рабочий хук, успешный вызов claude -p, реальное сгенерированное сообщение — и ничего в коммите.

Почему нельзя просто удалить паттерн

Первая мысль — убрать r"\bclaude code\b", ведь паттерн r"generated (with|by)\s+claude" должен ловить атрибуцию. Но не ловит. Реальный футер, который выводит Claude Code, выглядит так:

🤖 Generated with [Claude Code](https://claude.ai/code)

Квадратная скобка перед «Claude» разрывает регулярное выражение generated (with|by)\s+claude — оно ожидает пробел между «with» и «Claude», а находит [. Удаление r"\bclaude code\b" устраняет ложные срабатывания, но открывает дыру, через которую реальная атрибуция проходит в коммит незамеченной. Торг одного бага на другой.

Исправление: одна строка с контекстным ограничением

Финальное решение — заменить свободное совпадение на паттерн, требующий предлог перед названием инструмента и допускающий квадратные скобки:

r"\b(with|by|using|via)\s*\[?\s*claude code\]?"

Результаты тестирования:

Входная строка Результат
🤖 Generated with [Claude Code](url) Удалён (атрибуция поймана)
Co-Authored-By: Claude <noreply@anthropic.com> Удалён (атрибуция поймана)
generated by claude code Удалён (атрибуция поймана)
docs: add claude code hook install instructions Сохранён
feat: wire up claude code review workflow for PRs Сохранён
chore: document claude code slash commands in README Сохранён
fix: handle claude code MCP timeout in server.py Сохранён

Паттерн попал в оба файла — server.py и git_commit.py — потому что список _STRIP_PATTERNS дублируется копированием, а не импортом. Автор уже писал о рисках такого подхода к дрейфу кода, и этот случай — ещё одно подтверждение.

Что это значит на практике

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

  1. Поймать всё, что нужно. Фильтр должен срабатывать на реальные варианты атрибуции, включая markdown-разметку, нестандартные пробелы и пунктуацию.
  2. Не задеть то, что не нужно. Фильтр должен учитывать контекст проекта — в частности, тот факт, что название AI-инструмента может быть техническим термином в кодовой базе, которая этот инструмент использует.

«Я добавил границы слова, значит, теперь точно» — это не проверка. Проверка — это прогнать фильтр через реальную историю коммитов проекта, где слово «claude code» встречается и как атрибуция, и как имя CLI-утилиты.

Важный момент: ситуация, когда название инструмента совпадает с ключевыми словами атрибуции, будет возникать всё чаще. По мере того как AI-инструменты встраиваются в рабочий процесс разработки, их имена неизбежно попадут в сообщения коммитов, pull request'ы, документацию и README. Фильтры, которые не различают упоминание инструмента и подпись инструмента, будут ломаться всё чаще — и всё менее заметно.