claude-code
Пин модели у субагентов Claude Code срывается молча
Claude News
claude-codeClaude Code выбирает модель субагента по четырём уровням приоритета, и пин model: sonnet в frontmatter агента стоит на третьем из них, ниже явного параметра в самом вызове. Как пишет Thomas-witt, в нескольких релизах этот уровень молча переставал работать, и пиннутые агенты уходили на модель сессии.
Коротко
- Порядок разрешения такой: переменная окружения, явный параметр model в вызове, ключ в frontmatter агента и модель сессии; выпадение третьего уровня отправляет пиннутого агента прямо на четвёртый.
- Хук PreToolUse с матчером Task читает payload через jq, достаёт пин из frontmatter через awk и блокирует вызов кодом 2, если явного model в нём нет.
- Сбой не даёт ни ошибки, ни предупреждения: субагент возвращает нормальный результат, а разница между Sonnet и моделью сессии проявляется только в расходе токенов спустя недели, когда цифра уже набежала в статистике потребления.
Молчаливые регрессии стоимости плохо ловятся именно потому, что не выглядят как поломка: результат корректный, транскрипт обычный, а растёт только счёт. Механические субагенты по определению вызываются чаще всего и просматриваются реже всего, поэтому подмена модели у них, судя по всему, обнаруживается позже, чем что-либо ещё в сессии. Перенос пина на уровень вызова через хук выглядит как способ не полагаться на слой, который уже подводил.
Пин в frontmatter стоит третьим уровнем из четырёх
Claude Code разрешает модель субагента по четырём уровням: переменная окружения, параметр model в самом вызове, ключ model: в файле .claude/agents/<name>.md и модель текущей сессии. Frontmatter документирован и записывается один раз, поэтому именно им, по словам автора, пользуются чаще всего, но в очереди приоритетов он третий.
В файле .claude/agents/cloudwatch-digger.md пин занимает одну строку: model: sonnet. По описанию автора, в нескольких релизах этот уровень переставал срабатывать, и пиннутый агент проваливался на четвёртый уровень, то есть на модель сессии; у автора это Fable. Ошибки при этом не возникает, транскрипт выглядит как обычно.
На Sonnet автор держит агенты, чья работа сводится к схеме «запросить много, отфильтровать, вернуть мало»: запросы к CloudWatch Logs, GitHub, Honeybadger и Langfuse, отчёты RubyCritic и RuboCop, вывод тест-раннеров вроде RSpec. Смысл субагента, по его словам, в отдельном контекстном окне: шум остаётся там, а в основную сессию возвращаются несколько нужных строк.
Хук PreToolUse блокирует вызов кодом 2 и требует явной модели
Обходной путь, который сходится в баг-репортах, простой: передавать model прямо в вызове. Это второй уровень приоритета, и он, по словам автора, не ломался. Полагаться на то, что оркестратор будет помнить об этом каждый раз, автор не стал и вынес правило в хук.
Скрипт .claude/hooks/enforce-subagent-model.sh читает payload через jq, достаёт пин из frontmatter через awk и, если пин есть, а явного model в вызове нет, пишет в stderr сообщение BLOCKED с указанием нужной модели и завершается с кодом 2. Claude Code получает это сообщение и переотправляет вызов сам. Файл нужно сделать исполняемым через chmod +x.
Регистрируется хук в .claude/settings.json: блок hooks, событие PreToolUse, matcher Task и команда $CLAUDE_PROJECT_DIR/.claude/hooks/enforce-subagent-model.sh. Сам скрипт автор укладывает в девять строк. Проверка простая: отправить пиннутого агента без модели и получить BLOCKED, после чего следующий вызов уже несёт пин из frontmatter.
Хук пропускает inherit, встроенные типы и нечитаемый payload
Любой вызов с явным model проходит, даже если модель отличается от пина: хук ловит только пропуск параметра, осознанный выбор другой модели остаётся решением оркестратора. Агенты с model: inherit или вовсе без ключа model считаются наследующими намеренно. Встроенные типы вроде Explore, Plan и general-purpose файла в .claude/agents не имеют, и проверять у них нечего.
Если jq не смог разобрать payload, все три переменные оказываются пустыми и хук завершается нулём. Автор называет это осознанным выбором: механизм задуман как защита от лишних расходов, а не как граница безопасности, и хук, который падает закрыто на непонятном ему payload, рано или поздно застопорит сессию.
Пустое или null-значение model отдельным выбором не считается: такой вызов проваливается в тот же frontmatter, который и находится под подозрением, поэтому хук блокирует его наравне с вызовом без параметра. Автор предупреждает, что порядок приоритета в Claude Code меняется между релизами, и советует проверять поведение на своей версии.
Незакрытая брешь в workflow-инструментах
Внутренние вызовы agent() внутри workflow-инструментов не проходят через PreToolUse: хук покрывает только вызовы через Task и Agent. Эти случаи автор описывает текстом в справочных документах и признаёт, что фактически они не закрыты, а более чистое решение просит присылать. Отдельной темой он оставляет пост про сами субагенты для тех, кто их ещё не использует, и обещает написать его при интересе читателей.
Комментарии
Пока никто не написал. Будьте первым.
Присоединяйтесь к разговору
Войдите через Google, чтобы оставить комментарий. Имя и аватар подставятся из вашего профиля Google, а комментарий появится после модерации.
Из Google мы используем только имя и аватар. Почту не сохраняем.
