Хуки Claude Code в 2.1.141: три исправления вместо костылей

Три ошибки в хуках Claude Code исчезли за неделю: terminalSequence, args:string[] и continueOnBlock. Готовая настройка settings.json для продакшена.

Saturday, September 5, 2026Omid Saffari
Хуки Claude Code в 2.1.141: три исправления вместо костылей

В Q1 хуки Claude Code трижды подвели меня особенно болезненно. Все три ошибки незаметно исправили за 7 дней, а уже на следующее утро я удалил из админского репозитория три shell-обхода.

Хуки Claude Code: неделя, за которую исчезли три реальные ошибки

С 6 по 13 мая 2026 года Anthropic выпустила версии Claude Code от 2.1.132 до 2.1.141. Большая часть изменений пришлась на систему хуков.

Я обратил на это внимание из-за трёх костылей в своём репозитории. Рядом с каждым стоял комментарий // TODO: remove when claude-code fixes this, и за одну неделю я удалил их все.

Вот эти ошибки — в порядке нанесённого ущерба.

Первая — пропущенные уведомления на рабочем столе. Мой хук Notification отправлял системный сигнал в stdout, чтобы терминал оповещал меня после долгого уплотнения контекста. Пока Claude Code владел активным TTY, всё работало. В остальных случаях уведомление молча терялось: в разделённых окнах tmux, встроенных терминалах VS Code и панелях WezTerm, если фокус находился на соседней панели. Исправление появилось в 2.1.141 в виде terminalSequence.

Вторая — постоянно расползающееся экранирование shell-команды. Мой хук Stop запускал bash -lc "node post-stop.js --reason '$CLAUDE_STOP_REASON'", и стоило Claude вернуть строку с апострофом, как поле причины ломало команду. В 2.1.139 проблему решил exec-формат args: string[].

Третья — отклонения PostToolUse, которые завершали ход вместо того, чтобы вернуть управление Claude. Мне пришлось полностью отключить хук проверки схемы: среда выполнения воспринимала решение block как фатальную ошибку. В 2.1.139 появился continueOnBlock: теперь блокировка превращается в сигнал для повторной попытки, а её причина добавляется в контекст.

В конце приведён полный settings.json. Между делом — только необходимое.

Анатомия хуков за 90 секунд

Claude Code 2.1.x поддерживает девять событий хуков: SessionStart, PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact и SessionEnd. Каждый хук — это команда, которую среда выполнения запускает с документированным окружением и данными в stdin. stdout хука разбирается как JSON, а отдельные поля управляют поведением среды: decision (allow/deny/block), reason (строка, которую возвращают Claude или показывают пользователю), terminalSequence (необработанные байты, отправляемые в управляющий TTY) и несколько полей для конкретных событий.

Главное: среда выполнения считает stdout хука источником истины. Поле, которое она не разбирает, бесполезно. Поле, которое она распознаёт, меняет то, что Claude увидит на следующем ходу. В этом и заключается вся механика.

terminalSequence: уведомления на рабочем столе без активного TTY

До 2.1.141 мой хук Notification выглядел так:

JSON
{
  "hooks": {
    "Notification": [{
      "command": "bash -lc 'printf \"\\a\" && notify-send \"Claude\" \"$CLAUDE_MESSAGE\"'"
    }]
  }
}

Команда printf "\a" должна была включать звуковой сигнал терминала. На практике она записывала \a в файловый дескриптор, унаследованный процессом хука, а это не пользовательский терминал — если только Claude Code в этот момент не работает на переднем плане. Когда Claude запущен в панели 2 tmux, а я редактирую файл в панели 1, сигнал до внешнего терминала не доходил. notify-send срабатывал, но уведомление оставалось в панели GNOME, где я его часто не замечал.

В 2.1.141 в JSON, который хук выводит в stdout, появилось поле terminalSequence. Среда выполнения берёт эту строку и напрямую записывает её в устройство управляющего терминала, обходя stdio процесса хука. Вот версия после обновления:

JSON
{
  "hooks": {
    "Notification": [{
      "command": "node hooks/notify.mjs"
    }]
  }
}
JavaScript
import { execSync } from "node:child_process";

const payload = JSON.parse(
  await new Response(process.stdin).text()
);

execSync(`notify-send "Claude" ${JSON.stringify(payload.message)}`);

process.stdout.write(JSON.stringify({
  terminalSequence: ""
}));

 — это BEL. Среда выполнения записывает его в управляющий TTY, поэтому внешний терминал подаёт сигнал, даже когда Claude находится в фоновой панели. В моих тестах это корректно работает в macOS Terminal, iTerm2, WezTerm и Alacritty. tmux пропускает BEL; для OSC 52 в любом случае стоит оставить set -g allow-passthrough on в tmux.conf.

Есть один нюанс: встроенный терминал VS Code по умолчанию поглощает BEL. Чтобы сигнал срабатывал и там, добавьте "terminal.integrated.enableBell": true в пользовательские настройки.

args: string[] устранил экранирование shell-команд в моих хуках

Хук Stop срабатывает после завершения хода. Я использую его, чтобы записывать метаданные сессии в экземпляр Cloudflare D1, а затем искать по истории прошлых запусков.

Версия до 2.1.139:

JSON
{
  "hooks": {
    "Stop": [{
      "command": "bash -lc \"node scripts/post-stop.js --session $CLAUDE_SESSION_ID --reason '$CLAUDE_STOP_REASON'\""
    }]
  }
}

За первый месяц я столкнулся здесь с тремя ошибками экранирования.

Апостроф в $CLAUDE_STOP_REASON закрывал аргумент в одинарных кавычках, после чего остаток строки разбирался как shell-токены. Когда Claude возвращал причину вроде user's request completed, хук падал, а запись о сессии терялась.

Обратные кавычки в именах инструментов. Если хук срабатывал при значении $CLAUDE_TOOL_NAME, содержащем строку `bash` — потому что Claude использовал её в ответе, — shell пытался выполнить содержимое обратных кавычек в подоболочке. В этом случае безобидно, но в целом пугающе.

Unicode в пользовательских промптах. Обычно UTF-8 без проблем проходит туда и обратно через bash -lc, однако при некоторых сочетаниях кодовых точек CJK и настроек локали байты незаметно терялись.

В 2.1.139 появился exec-формат команд. Передайте args как массив строк, и среда выполнения запустит команду напрямую, без промежуточного shell:

JSON
{
  "hooks": {
    "Stop": [{
      "args": [
        "node",
        "scripts/post-stop.js",
        "--session", "$CLAUDE_SESSION_ID",
        "--reason", "$CLAUDE_STOP_REASON"
      ]
    }]
  }
}

Перед вызовом execve среда выполнения сама подставляет переменные $CLAUDE_* из своего окружения. Нет shell — нет shell-интерполяции и проблем с кавычками. Строка user's request completed попадает в мой Node-скрипт единым элементом argv[5], в точности как её сформировал Claude.

Для хука PreToolUse, который запускается при каждом вызове инструмента, это существенно.

Когда всё же нужен shell-формат: если команда использует каналы, перенаправления или glob-паттерны. В одной записи хука args:[] и command:"" взаимоисключающие, поэтому для node x.js | jq | tee log оставьте command:"" и учитывайте цену экранирования. Для 90% хуков правильный выбор — exec-формат.

continueOnBlock: отклонение PostToolUse, которое действительно запускает повтор

Этого исправления я ждал дольше всего. У меня есть хук PostToolUse, который проверяет результат любого вызова инструмента, записывающего данные на диск: если Claude создаёт файл TypeScript, хук запускает для него tsc --noEmit и отклоняет результат при наличии ошибок типов.

До 2.1.139 сценарий отклонения был сломан:

JSON
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "command": "node hooks/validate-ts.mjs"
    }]
  }
}

Когда validate-ts.mjs возвращал { "decision": "block", "reason": "tsc failed: ..." }, среда выполнения завершала ход. Claude не видел причину. Пользователь получал малопонятное сообщение "hook blocked the operation" и был вынужден вручную отправлять новый промпт, вставив туда текст ошибки. После трёх рабочих сессий, сорванных таким образом, я отключил хук.

В 2.1.139 появился параметр continueOnBlock для отдельного хука:

JSON
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "args": ["node", "hooks/validate-ts.mjs"],
      "continueOnBlock": true,
      "maxAttempts": 3
    }]
  }
}

Теперь решение block возвращает строку reason в контекст Claude как ошибку результата инструмента. Claude видит tsc failed: src/api.ts(14,3): error TS2322: Type 'string' is not assignable to type 'number' и самостоятельно исправляет проблему на следующем ходу. Пользователь ничего не замечает: цикл выполняется внутри.

maxAttempts задаёт жёсткий предел. Без него недетерминированная проверка — например, зависящая от периодически недоступного удалённого API — будет бесконечно расходовать контекст на повторные попытки. Я использую значение три. После трёх неудач хук переходит к жёсткой блокировке и показывает ошибку пользователю.

Антипаттерн: не включайте continueOnBlock для хуков, чьи решения зависят от текущего времени. Хук, запрещающий запись во время окна развёртывания, уйдёт в бесконечный цикл, если при повторной попытке Claude развёртывание ещё продолжается. Либо проверяйте $CLAUDE_EFFORT, либо добавьте счётчик попыток в скрипт хука.

Бонусом: $CLAUDE_EFFORT и CLAUDE_PROJECT_DIR

В тот же период незаметно добавили две переменные окружения, и обе оказались действительно полезными.

Начиная с 2.1.133, $CLAUDE_EFFORT передаётся в окружение хуков. Возможные значения — low, medium, high и xhigh; они соответствуют уровню усилий Claude для текущего хода. Благодаря этому хук может выбирать ветку по уровню усилий, не разбирая промпт:

JavaScript
const effort = process.env.CLAUDE_EFFORT;

if (effort === "low" || effort === "medium") {
  // skip expensive tsc check on quick edits
  process.stdout.write(JSON.stringify({ decision: "allow" }));
  process.exit(0);
}

// run full tsc --noEmit on high/xhigh

На быстрых правках я экономлю около 800ms, пропуская tsc при низком уровне усилий. На планировочном ходу с уровнем high, когда Claude записывает дюжину файлов, полная проверка по-прежнему запускается и находит реальные ошибки.

В 2.1.139 переменная CLAUDE_PROJECT_DIR появилась в окружении stdio-серверов MCP, которые запускает среда выполнения. Раньше такие серверы определяли корень рабочего пространства через process.cwd(), и это ломалось, если пользователь запускал Claude Code из подкаталога. Теперь любой MCP-сервер может прочитать process.env.CLAUDE_PROJECT_DIR и корректно разрешить пути относительно рабочего пространства.

Если вы поддерживаете MCP-сервер, обновите логику разрешения путей в манифесте: используйте CLAUDE_PROJECT_DIR, а для старых клиентов оставьте запасной вариант с cwd(). Две строки кода — и целый класс ошибок исчезает.

Мой рабочий settings.json без сокращений

Это рабочий блок из omidsaffari-admin, лишь слегка отредактированный. Шесть DOs и один Workflow используют вывод хуков для уведомлений на рабочем столе и проверок CI.

JSON
{
  "model": "claude-sonnet-4-7-20260501",
  "permissions": {
    "edit": "ask"
  },
  "hooks": {
    "SessionStart": [{
      "args": ["node", "hooks/session-start.mjs"]
    }],
    "PreToolUse": [{
      "matcher": "Bash",
      "args": ["node", "hooks/gate-bash.mjs"]
    }],
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "args": ["node", "hooks/validate-ts.mjs"],
      "continueOnBlock": true,
      "maxAttempts": 3
    }],
    "Notification": [{
      "args": ["node", "hooks/notify.mjs"]
    }],
    "Stop": [{
      "args": [
        "node",
        "scripts/post-stop.js",
        "--session", "$CLAUDE_SESSION_ID",
        "--reason", "$CLAUDE_STOP_REASON"
      ]
    }],
    "PreCompact": [{
      "args": ["node", "hooks/notify.mjs"]
    }]
  }
}
JSON
{
  "devDependencies": {
    "@anthropic-ai/claude-code": "2.1.141"
  }
}

Установка и проверка:

Bash
pnpm add -D @anthropic-ai/claude-code@2.1.141
claude --version    # expect: 2.1.141
claude config doctor    # expect: 0 hook warnings

Главное здесь: continueOnBlock и maxAttempts образуют механизм повторных попыток из 2.1.139. Формат args:[] используется для каждого хука, которому не требуются возможности shell, — то есть для всех хуков в этой конфигурации. Хуки Notification и PreCompact используют общий notify.mjs, поскольку обоим нужны одинаковые уведомления на рабочем столе через terminalSequence.

Чек-лист обновления и случаи, когда лучше пропустить версию

Перед началом сохраните снимок текущего settings.json. Отметьте, какие хуки используют command:"", а какие уже переведены на args:[]. Составьте список всех подключённых типов событий.

Переносите по одному типу событий за раз. После каждого изменения дайте конфигурации поработать 24 часа. Следите за предупреждениями хуков в claude config doctor и ищите в логах строки decision и reason, чтобы убедиться, что среда выполнения получает ожидаемые данные.

Я бы шёл в таком порядке:

  1. Обновить пакет до 2.1.141.
  2. Перевести один хук с command:"" на args:[]. Проверить его срабатывание.
  3. Добавить terminalSequence в хук Notification. Проверить сигнал из фоновой панели tmux.
  4. Добавить continueOnBlock в самый проблемный хук PostToolUse. Проследить за одной реальной сессией и убедиться, что Claude видит причину и самостоятельно исправляет ошибку.
  5. Пакетно перевести остальные хуки на args:[].

Не обновляйтесь, если используете управляемую установку, зафиксированную на версии родительского SDK, который ещё не проверен с 2.1.141, либо если зависите от поля хука, помеченного в 2.1.141 как устаревшее. На момент написания среди майских изменений не было ничего, что нарушало бы совместимость существующих полей, но перед внедрением для команды зафиксируйте версию и проверьте её в отдельной ветке.

Полный плейбук по фиксации версий, стратегии хуков и подготовке проектов для шести моих рабочих агентов собран в чек-листе настройки Claude Code + Codex. Там используются те же паттерны settings.json, а также показана инфраструктура на стороне агентов — Workflows, DOs и привязки Vectorize, которые контролируют хуки.

Сравнение инструментов для запуска этих агентов в удалённых песочницах — в материале о средах Cursor Cloud Agent и Cloudflare Workers. Контекст рабочего стека для этого settings.json разобран в статье о продакшен-агентах Cloudflare для 100x-инженера.

Работает ли terminalSequence в tmux?

Да, но есть один нюанс. Среда выполнения записывает последовательность в устройство управляющего терминала, а tmux передаёт её внешнему терминалу, если в tmux.conf указано set -g allow-passthrough on или последовательность представляет собой обычный BEL (). BEL проходит без дополнительных условий. Для последовательностей OSC нужно разрешить passthrough.

Можно ли использовать args:[] и command:'' в одной конфигурации хука?

Нет. В одной записи хука эти формы взаимоисключающие. Для прямого запуска команды выбирайте exec-формат (args:[]), а если нужны каналы, перенаправления или glob-паттерны — shell-формат (command:""). Если требуется совместить оба подхода, создайте скрипт-обёртку: вызывайте его через exec-формат, а shell-возможности реализуйте внутри.

Будет ли continueOnBlock повторяться бесконечно, если Claude снова и снова не проходит одну проверку?

Нет, если задать maxAttempts. Среда выполнения ограничит число повторов этим значением, а затем перейдёт к жёсткой блокировке. Без maxAttempts недетерминированный валидатор действительно может впустую расходовать контекст. Для проверок типов разумное значение по умолчанию — три; для проверок, зависящих от удалённого состояния, — один.

Доступна ли $CLAUDE_EFFORT во всех событиях хуков?

Да. Начиная с 2.1.133 эта переменная передаётся в окружение каждого хука, который запускает среда выполнения. Значение соответствует уровню усилий для текущего хода: SessionStart получает уровень, с которым пользователь начал работу, а PostToolUse — уровень, активный на момент вызова инструмента.

Что сломается после отката до 2.1.138?

terminalSequence, args:[], continueOnBlock и CLAUDE_PROJECT_DIR молча перестанут работать. Среда выполнения игнорирует неизвестные поля JSON и возвращается к разбору command:"". Хуки продолжат запускаться, но новые возможности исчезнут. Прежде чем полагаться на откат, проверьте его в отдельной ветке.

Последнее обновление
5 сент. 2026 г.
Категория
Build

Сделать этот сайт предпочтительным в Google

Добавить omidsaffari.com как предпочтительный источник в Google Поиске

Отметьте omidsaffari.com как предпочтительный источник — и Google будет поднимать его для вас в Top Stories, AI Overviews и AI Mode.

Похожие статьи
Cursor Rollouts бесплатно? Что дают стартовые кредиты

Cursor Rollouts бесплатно? Что дают стартовые кредиты

Разбираем, доступен ли Cursor Rollouts бесплатно, кому дают кредиты на 10 дней, сколько стоит Teams и что известно о цене после их окончания.24 сент. 2026 г.Build
Как использовать Unreal Agent: тест CLI-раннера на репозитории

Как использовать Unreal Agent: тест CLI-раннера на репозитории

Разбираем, как запустить Unreal Agent на одной задаче в репозитории: установка Go, ключи провайдера, JSONL-логи, сессии, расходы и границы безопасности.24 сент. 2026 г.Build
JetBrains Air: настройка и первый запуск ИИ-агента

JetBrains Air: настройка и первый запуск ИИ-агента

Разбираемся, как установить JetBrains Air, подключить ИИ-агента, передать ему контекст проекта и безопасно проверить первое изменение в коде.23 сент. 2026 г.Build
Цена JetBrains Air: бесплатный плагин и расходы на ИИ

Цена JetBrains Air: бесплатный плагин и расходы на ИИ

Плагин JetBrains Air бесплатен, но за IDE, ИИ-агента, API или кредиты может платить другой аккаунт. Сравниваем Junie Lite и тарифы JetBrains AI.23 сент. 2026 г.Build
Самостоятельный хостинг Firecrawl: установка, проверка и реальные расходы

Самостоятельный хостинг Firecrawl: установка, проверка и реальные расходы

Разбираем самостоятельный хостинг Firecrawl: как развернуть и проверить стек, какие функции доступны и почему Cloud дешевле при 1,000–10,000 страницах.22 сент. 2026 г.Build
Контроль ИИ-агентов: платный повтор требует решения человека

Контроль ИИ-агентов: платный повтор требует решения человека

ИИ-агент потратил $5.48 до проверки человеком. Разбираем, почему платный повтор требует отдельного разрешения, которое модель не может выдать себе сама.22 сент. 2026 г.Build
Конструктор ИИ-агентов MindStudio: цены, возможности и ограничения

Конструктор ИИ-агентов MindStudio: цены, возможности и ограничения

Подробный разбор MindStudio: кому подходит конструктор ИИ-агентов без кода, сколько он стоит, где его пределы и как провести проверку на 20 записях.22 сент. 2026 г.Build
Superwhisper или Wispr Flow: что лучше для диктовки?

Superwhisper или Wispr Flow: что лучше для диктовки?

Superwhisper или Wispr Flow: сравниваем цены, локальную обработку, платформы и функции для команд, чтобы понять, какой сервис выбрать для диктовки.22 сент. 2026 г.Build
Рассылка

Одно письмо, каждое воскресенье.Работающие системы, а не горячие мнения.

Еженедельно. Без спама. Отписка в любой момент.