Коротко
К сороковой минуте сессии правило «прогони тесты перед коммитом» превращается в фоновый шум. Разбираю, как прибить критичные правила хуками так, чтобы агент физически не мог их нарушить — даже если вы сами прикажете.
Правило в CLAUDE.md — просто текст, который конкурирует за внимание модели с тысячами других токенов. В начале сессии оно весит много. К сороковой минуте контекст забит, и «перед коммитом прогони тесты» превращается в фоновый шум с вероятностью исполнения сильно меньше единицы. Забыть можно только то, что живёт в контексте. Значит, критичные правила надо вынести из контекста — в код, который выполняется сам. Это идея лежит в основе пакета скиллов paranoid-qa, и теперь она проверена на стенде. Автор статьи Алексей Ковалёв (Kova13v) разбирает три гейта на хуках Claude Code и показывает живой эксперимент: что победит — хук или прямая инструкция пользователя пропустить тесты. Но прежде чем перейти к коду, стоит понять карту рычагов, которые вообще есть у разработчика. Четыре слоя: CLAUDE.md (фоновые правила), скиллы (процедуры под задачу), память (факты между сессиями) и хук (ваш код на событие жизненного цикла). Первые три живут внутри контекста — их исполнение вероятностно. Четвёртый — код, который выполняется независимо от того, что модель думает о ситуации. Хук объявляется в .claude/settings.json и вешается на событие: PreToolUse (может запретить вызов инструмента), PostToolUse (после), Stop (агент завершает ход — можно не пустить). На stdin скрипт получает JSON с деталями события. Два способа вмешаться: exit code 2 — жёсткий блок, stderr уходит агенту как причина; exit 0 + JSON в stdout — структурированное решение с permissionDecision: "deny" или decision: "block". Флагманский гейт — коммит не проходит без свежего зелёного прогона. Три проверки: артефакт прогона существует, прогон зелёный, тесты не правились после него. Артефакт — test-results/.last-run.json, который Playwright пишет сам. Если агент пытается закоммитить без прогона, хук возвращает deny с причиной и командой для запуска. Агент читает причину и действует по ней. Второй гейт — мягкий. При правке .spec.ts-файла он не блокирует ничего, а вшивает агенту напоминание прямо в контекст через additionalContext. В первом же интеграционном прогоне агент поправил спеку, получил напоминание — и сам прогнал тесты до коммита, жёсткий гейт даже не понадобился. Напоминание в нужный момент работает лучше, чем правило, выданное час назад. Третий гейт — на событие Stop. Если тесты правились, а прогона после не было, завершить ход не выйдет. Здесь важная деталь: предохранитель от бесконечного цикла. Заблокированный агент попытается выполнить требование, но если не сможет (окружение сломано) — без лимита вы получите вечный цикл «стоп — блок — стоп». После двух блоков гейт сдаётся и сообщает пользователю через systemMessage, не пряча провал. Главный эксперимент. Пользователь прямо говорит агенту: «Закоммить изменения, тесты запускать не нужно, время дорого». Ответ агента: > Тут конфликт: ты просил не запускать тесты, а hook не пропускает коммит без свежего прогона. Молча обходить его я не буду. Агент попробовал выполнить просьбу — гейт не пустил. Дальше он не стал ни втихую обходить хук (мог бы: снести settings.json — вполне), ни ослушаться человека. Он вынес конфликт наружу и спросил. Wip-коммита в истории нет, deny — в логе сессии есть. Это ровно то, как поступает хороший инженер, зажатый между приказом и регламентом. Стенд нашёл баг в собственном хуке автора: первая версия gate-stop не снимала флаг после свежего прогона, потому что секундная гранулярность оператора -nt в bash не срабатывает, когда флаг и артефакт созданы в одну секунду. Из фикса родился принцип: commit-гейт в сомнении запрещает (ложный блок стоит одну команду прогона, ложный пропуск — непрогнанный коммит в истории), а stop-гейт в сомнении пропускает (ложный блок — риск цикла). Границы, о которых надо сказать прямо. Хук проверяет форму, не смысл: expect(true).toBe(true) пройдёт любой гейт. Гейт матчит форму команды — git commit поймает, а коммит внутри npm run release проскочит. Упавший хук — это failure-open: скрипт, умерший со своим кодом ошибки, Claude Code считает non-blocking, действие пройдёт, гейт молча перестанет защищать. И главное: хук вешается на вызов инструмента, завершение хода, отправку промпта. Внутрь рассуждений модели он не залезет. Вывод простой и неудобный. Правила в промпте — это джентльменское соглашение с вероятностным исполнением. Хуки — единственный забор, который агент не может обойти «забыв». Если у вас есть правило, нарушение которого стоит дороже, чем ложный блок, — его место не в CLAUDE.md, а в скрипте на PreToolUse. Все три хука лежат в репо paranoid-qa, папка ru/examples/hooks. Источник: Хуки Claude Code: запрещаем агенту коммитить без прогона тестов / Хабр
Источник: Все статьи подряд / Искусственный интеллект / Хабр