Skip to content

Хуки ​

Хуки — это shell-скрипты, которые Command Code запускает автоматически до и после каждого вызова инструмента. Они дают вам детерминированный контроль над поведением Command Code. Используйте хуки, чтобы обеспечивать соблюдение правил проекта, аудировать использование инструментов и связывать Command Code с процессами, которые у вас уже есть.

Что такое хуки? ​

Хуки срабатывают до запуска shell-команды, до чтения файла и после записи. Каждый хук получает входные данные инструмента как JSON на stdin и возвращает одно из четырёх действий: разрешить, отклонить, остановить сессию или вставить дополнительный контекст для следующего хода.

Что можно делать с хуками? ​

С настроенными хуками вы можете:

  • Блокировать опасные bash-команды до их выполнения, чтобы rm -rf / никогда не прошёл мимо защиты политики.
  • Вставлять контекст конкретного проекта, когда агент читает чувствительные файлы, подталкивая модель вычищать секреты.
  • Аудировать каждую запись на диск, отправляя пути файлов в локальный лог.
  • Останавливать сессию, если вывод инструмента утёк чувствительную информацию.

Поддерживаемые события ​

СобытиеКогда срабатываетМожет блокировать?
PreToolUseДо запуска инструментаДа (отклоняет инструмент)
PostToolUseПосле возврата инструментаНет (советующее)
StopКогда ассистент завершает ходДа (принудительный пересмотр, не более 3 повторов)
SessionStartКогда начинается сессия (старт, возобновление, очистка)Нет (только вставка контекста)

Примечание

Нужно реагировать на большее число моментов жизненного цикла сессии — активность субагентов, пользовательские инструменты или другие моменты помимо этих четырёх событий? Для этого есть моды.

Поддерживаемые инструменты ​

ИнструментЛитерал matcherЧто покрывает
ShellshellВыполнение shell-команд
ReadreadЧтение файлов
WritewriteСоздание файла или полная перезапись
EditeditПравки на месте в существующем файле

Синтаксис matcher (regex, нечувствительность к регистру, сопоставление нескольких инструментов) — в разделе Схема настроек.

Как работают хуки ​

Хук — это любая исполняемая программа, которая читает JSON-нагрузку на stdin и пишет JSON-объект в stdout. Пишите его на bash, Node.js, Python или чём угодно, что умеет разбирать JSON.

  1. Событие произошло: Command Code собирается запустить (или только что запустил) инструмент.
  2. Хук вызван: Command Code запускает ваш исполняемый файл и пишет детали инструмента как JSON в его stdin.
  3. Хук отвечает: ваш исполняемый файл пишет JSON-ответ в stdout, чтобы разрешить, отклонить, вставить контекст или остановить сессию.

Схема stdin и stdout, коды выхода, переменные окружения и семантика исполнения описаны в справочных разделах ниже.

Быстрый старт ​

Этот быстрый старт подключает ваш первый хук в Command Code.

1. Создайте файл настроек ​

Создайте .commandcode/settings.json в корне проекта и вставьте сниппет ниже. Хук PreToolUse срабатывает перед каждой shell-командой и печатает сообщение.

.commandcode/settings.json:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "shell",
        "hooks": [{ "type": "command", "command": "echo '{\"systemMessage\":\"hook fired\"}'" }]
      }
    ]
  }
}

Поле command — это то, что Command Code запускает, когда хук срабатывает. Здесь он печатает JSON-объект с полем systemMessage, и Command Code показывает это сообщение при выполнении shell-команд.

2. Попробуйте ​

Хуки инициализируются при старте. Перезапустите Command Code командой cmd, затем выполните промпт вроде Use the shell tool to list the files in the current directory. Сообщение PreToolUse: hook fired должно появиться до выполнения shell-команды.

Хук из быстрого старта выдаёт статичное сообщение. Настоящие хуки разбирают JSON-нагрузку на stdin и возвращают результаты, останавливают сессии или вставляют контекст для следующего хода — читайте дальше про настройку, полную схему, примеры и лучшие практики.

Настройка ​

Хуки настраиваются под ключом hooks в файле settings.json. Command Code ищет settings.json по следующим путям:

ОбластьФайл конфигурацииК чему применяетсяКоммитится в git?
Пользователь~/.commandcode/settings.jsonВо всех проектахНет
Проект.commandcode/settings.jsonВсе, кто работает с проектомДа

Приоритет: проект > пользователь.

Когда одна и та же строка команды встречается в нескольких областях, побеждает источник с более высоким приоритетом.

Порядок ​

Внутри одного события хуки срабатывают в том порядке, в каком они записаны в settings.json (сначала проект, затем пользователь).

Например, хуки PreToolUse выполняются последовательно. Как только один заблокировал инструмент, остальные хуки PreToolUse пропускаются. Хуки PostToolUse выполняются параллельно, потому что инструмент уже завершился.

Несколько хуков можно подключить под одним matcher. Они выполняются в порядке перечисления:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "shell",
        "hooks": [
          { "type": "command", "command": "./.commandcode/hooks/guard-bash.sh", "timeout": 5 },
          { "type": "command", "command": "./.commandcode/hooks/log-shell.sh" }
        ]
      }
    ]
  }
}

Здесь guard-bash.sh выполняется первым. Если он отклоняет вызов, log-shell.sh пропускается.

Полный settings.json, подключающий несколько matcher в обоих событиях, — в разделе Пример settings.json.

Схема настроек ​

Хуки настраиваются под ключом hooks в settings.json. Массив каждого события имеет два уровня вложенности: HookDefinition выбирает, к каким инструментам относится хук (через matcher), а HookEntry — это обработчик, который их запускает.

text
settings.json
└── hooks
    └── <EventName>
        └── [ HookDefinition ]
              │ matcher: which tools
              └── hooks: [ HookEntry ]
                    type · command · timeout

Один HookDefinition может владеть несколькими обработчиками HookEntry. Все они выполняются для одного matcher в порядке перечисления.

Поля HookDefinition (внешние) ​

Выбирает, к каким инструментам относится эта группа обработчиков.

ПолеОбязательноТипОписание
matcherНеобязательноstringНе указывайте, чтобы совпадать с любым инструментом. Примеры: "shell", "write|edit". Имеет смысл только для событий инструментов (PreToolUse, PostToolUse). Stop и SessionStart не несут инструмента, поэтому matcher там никогда не совпадёт и хук не сработает — не указывайте его для этих событий.
hooksОбязательноarrayОдин или несколько обработчиков. Выполняются в порядке перечисления

Поля HookEntry (внутренние) ​

Описывает один обработчик для запуска.

ПолеОбязательноТипОписание
typeОбязательноstringВид обработчика. Поддерживается только адаптер command
commandОбязательно при type: "command"stringShell-команда для выполнения
timeoutНеобязательносекундыПо умолчанию 30, максимум 600

Пример settings.json ​

В примере ниже хук PreToolUse ограничивает десятисекундную защиту вызовами инструментов shell и write. Хук PostToolUse не указывает timeout (по умолчанию 30 с) и аудирует каждый инструмент после его запуска.

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "shell|write",
        "hooks": [
          {
            "type": "command",
            "command": "./.commandcode/hooks/guard-tools.sh",
            "timeout": 10
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./.commandcode/hooks/audit.sh"
          }
        ]
      }
    ]
  }
}

Ввод хука (stdin) ​

Перед запуском хука Command Code пишет в его stdin один JSON-объект. Прочитайте stdin до конца, разберите его как JSON, затем напишите свой ответ в stdout.

Общие поля ​

Присутствуют во всех событиях.

ПолеТипОписание
session_idstringИдентификатор сессии, стабильный на всё время жизни одной CLI-сессии
transcript_pathstringАбсолютный путь к JSONL-транскрипту этой сессии
cwdstringАбсолютный рабочий каталог на момент срабатывания
hook_event_namestringСобытие, вызвавшее хук.
permission_mode"default" | "auto-accept" | "plan" | "bypass" | "dont-ask" | ""Текущий режим разрешений. Пустая строка, когда режим не применяется.

Поля вызова инструмента ​

Присутствуют в каждом событии, связанном с вызовом инструмента.

ПолеТипОписание
tool_use_idstring?Стабильный id вызова инструмента. Присутствует в каждом настоящем вызове
tool_namestringКанонический id инструмента (shell_command, read_file, write_file, edit_file)
tool_display_namestringОдно из SHELL, READ, WRITE, EDIT. Значение, с которым проверяется matcher
tool_inputobjectАргументы инструмента в том виде, как их выдала модель. Форма зависит от инструмента, см. ниже

Поля tool_input ​

Форма tool_input зависит от того, какой инструмент сработал. Хуки читают эти поля, чтобы изучить вызов. Например, защита shell проверяет tool_input.command, а аудит записи читает tool_input.file_path.

ИнструментПоля
shell_commandcommand: string, args?: string[], directory?: string, timeout?: number
read_fileabsolute_path: string, offset?: number, limit?: number
write_filefile_path: string, content: string
edit_filefile_path: string, old_value: string, new_value: string, replacement_count?: number, replace_all?: boolean

Поля конкретных событий ​

Событие может добавлять собственные поля поверх общих и полей вызова инструмента. Новые события появляются со временем; каждое описано в подразделе ниже.

PostToolUse ​

ПолеТипОписание
tool_responsestringВывод инструмента, тот же текст, который увидит модель

Stop ​

Stop срабатывает, когда ассистент выдаёт финальный ответ без оставшихся вызовов инструментов (конец хода). Он не несёт полей инструмента — не указывайте matcher в хуках Stop (matcher здесь не даст хуку сработать). Он предоставляет только общие поля плюс:

ПолеТипОписание
stop_hook_activebooleantrue, когда это срабатывание само является повтором, вызванным предыдущим ответом хука Stop с decision: "block" или кодом выхода 2. Авторы хуков проверяют это и выходят с 0, чтобы вырваться из цикла повторов

SessionStart ​

SessionStart срабатывает один раз в начале сессии. Он не несёт полей инструмента — не указывайте matcher в хуках SessionStart (matcher здесь не даст хуку сработать). Он предоставляет только общие поля плюс:

ПолеТипОписание
source"startup" | "resume" | "clear"Почему началась сессия: свежий запуск, открытие существующей сессии или очистка разговора

Переменные окружения ​

Command Code внедряет четыре переменные окружения в каждый процесс хука.

ПеременнаяЗначение
COMMANDCODE_PROJECT_DIRАбсолютный путь к проекту (тот же, что cwd)
COMMANDCODE_SESSION_IDId сессии, полезен для соотнесения хуков с запуском
COMMANDCODE_HOOK_EVENTPreToolUse, PostToolUse, Stop или SessionStart
COMMANDCODE_CWDПсевдоним COMMANDCODE_PROJECT_DIR с идентичным значением

Ваши переменные окружения передаются процессам хуков, при этом чувствительные переменные вырезаются.

Вывод хука (stdout) ​

Исполняемый файл хука пишет в stdout один JSON-объект. Все поля необязательны. Пустой stdout при коде выхода 0 означает «мнения нет, разрешить».

Общие поля вывода ​

ПолеТипОписание
continuebooleanfalse останавливает сессию после текущей пачки инструментов. Используйте вместе с stopReason
stopReasonstringСообщение для пользователя, показываемое в TUI при continue: false. Модели не отправляется
suppressOutputbooleanКогда true, разобранный вывод хука не попадает в аудит-лог
systemMessagestringСвободное текстовое уведомление, показываемое в ленте TUI. Модели не отправляется

Поля PreToolUseOutput ​

Добавляет объект hookSpecificOutput поверх общих полей.

ПолеТипОписание
hookSpecificOutput.hookEventName"PreToolUse"Необязательно. Помогает человеку отличить форму PreToolUse; движок и так знает, какое событие сработало
hookSpecificOutput.permissionDecision"allow" | "deny""deny" блокирует инструмент. Не указывайте поле или ставьте "allow", чтобы разрешить
hookSpecificOutput.permissionDecisionReasonstringПоказывается модели при отказе. Используйте это, чтобы научить модель не повторять попытку
hookSpecificOutput.additionalContextstringДобавляется к результату инструмента перед следующим ходом модели

Полная форма:

json
{
  "continue": true,
  "suppressOutput": false,
  "stopReason": "",
  "systemMessage": "",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "",
    "additionalContext": ""
  }
}

Поля PostToolUseOutput ​

Добавляет decision / reason верхнего уровня и меньший hookSpecificOutput.

ПолеТипОписание
decision"block"Советующий сигнал повтора модели. Инструмент уже выполнился, поэтому ничего не отменяется
reasonstringРаботает в паре с decision: "block"
hookSpecificOutput.hookEventName"PostToolUse"Необязательно. Помогает читателю отличить форму PostToolUse; движок и так знает, какое событие сработало
hookSpecificOutput.additionalContextstringДобавляется к результату инструмента перед следующим ходом модели

Полная форма:

json
{
  "continue": true,
  "suppressOutput": false,
  "stopReason": "",
  "systemMessage": "",
  "decision": "block",
  "reason": "",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": ""
  }
}

Поля StopOutput ​

Stop использует только поля верхнего уровня, без hookSpecificOutput.

ПолеТипОписание
decision"block"Не даёт ассистенту завершить работу. Цикл агента делает ещё одну итерацию. Не более 3 повторов на ход
reasonstringПоказывается пользователю и передаётся модели при повторе, обёрнутый в рамку, чтобы читаться как отзыв на ответ. Для сырой диагностики, которую модель обрабатывает как вывод инструмента, используйте exit 2 + stderr

Полная форма:

json
{
  "continue": true,
  "suppressOutput": false,
  "stopReason": "",
  "systemMessage": "",
  "decision": "block",
  "reason": ""
}

Поля SessionStartOutput ​

SessionStart не блокирует: у него нет decision / reason верхнего уровня, и он никогда не останавливает, не повторяет и не отклоняет. Он несёт универсальные поля плюс блок hookSpecificOutput для вставки контекста.

ПолеТипОписание
hookSpecificOutput.hookEventName"SessionStart"Необязательно. Помогает читателю отличить форму SessionStart; движок и так знает, какое событие сработало
hookSpecificOutput.additionalContextstringВставляется в первый ход пользователя сессии, так что модель видит его до начала работы

Когда несколько хуков SessionStart возвращают additionalContext, значения соединяются и вставляются вместе.

Полная форма:

json
{
  "suppressOutput": false,
  "systemMessage": "",
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": ""
  }
}

Защита от зацикливания ​

Наивный хук Stop, который всегда возвращает decision: "block", зациклился бы навсегда. Два уровня защиты:

  1. stop_hook_active на входе равен true при срабатывании-повторе. Авторам хуков следует делать exit 0, когда они это видят.
  2. Жёсткий предел в 3 повтора на ход, который обеспечивает движок. После предела ход завершается строкой Stop hook retry cap reached (3), называющей виновный скрипт, чтобы вы могли его исправить или отключить.
bash
# canonical Stop hook skeleton
INPUT=$(cat)

if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0 # Already retrying, let the turn end
fi

# ... your check here ...

Передача текста модели при повторе ​

Оба триггера повтора передают текст модели перед следующим ходом. Разговор не может закончиться ходом ассистента, поэтому повтор всегда несёт сообщение роли user:

  • decision: "block" + reason: reason оборачивается в рамку («хук Stop попросил вас доработать…»), чтобы модель восприняла его как отзыв на свой предыдущий ответ, а не как новый запрос.
  • exit 2 + stderr: весь stderr (с ограничением, так что многострочная диагностика tsc/линтера проходит целиком) передаётся как есть, чтобы модель восприняла его как вывод инструмента и могла сразу на него реагировать:
bash
echo "tsc: 3 errors found" >&2
exit 2

Используйте reason для рекомендаций на естественном языке; используйте exit 2 + stderr для машинной диагностики, с которой модель должна работать дословно.

Кто видит каждое поле ​

По этой таблице выбирайте поле под ту аудиторию, до которой хотите достучаться.

ПолеПользователь (TUI)Модель
stopReasonданет
systemMessageданет
permissionDecisionReason (Pre)дада, при отказе
reason (Post / Stop)дада (Post и Stop при decision: "block"; при повторе через exit 2 вместо него идёт stderr)
additionalContextнетда, добавляется перед следующим ходом
stderr (exit 2)да, первая строкада (полный текст передаётся модели при повторе, все события)

Правило: для машинных деталей, с которыми модель должна работать дословно, используйте exit 2 + stderr. Для рекомендаций на естественном языке — reason хука Stop. Для уведомления только пользователю — systemMessage или stopReason.

Коды выхода ​

Код выхода — это быстрый путь. Большинство хуков использует только 0.

КодОбработка stdoutЭффект на инструментЭффект на сессию
0Разбирается как JSONОпределяется выводом (см. матрицу решений)Продолжается (если не continue: false)
2ИгнорируетсяPreToolUse: заблокирован. PostToolUse: советующий сигнал повтора. Stop / SessionStart: н/дPreToolUse / PostToolUse: продолжается. Stop: повторяет ход. SessionStart: продолжается (не блокирует)
любой другойРазбирается, если естьИнструмент выполняется, ошибка логируется без блокировкиПродолжается

Определение причины блокировки при коде выхода 2 ​

Текст, отправляемый модели, когда хук выходит с кодом 2, определяется в таком порядке:

  1. hookSpecificOutput.permissionDecisionReason (если stdout разбирается и задаёт его)
  2. reason верхнего уровня (только PostToolUse)
  3. Обрезанная первая строка stderr
  4. Общий текст-заглушка с именем хука и его кодом выхода

Обработка stdout при коде выхода 0 ​

  • Пустой stdout или stdout из одних пробелов означает «мнения нет», и инструмент выполняется.
  • Непустой stdout, который не разбирается как JSON или разбирается, но не проходит валидацию по схеме, логируется как предупреждение, и инструмент выполняется.

Матрица решений: блокировка и остановка ​

Каждый результат хука даёт два эффекта: выполняется ли инструмент и продолжается ли сессия после него.

СигналЗначениеИнструментСессия
exit code2 (PreToolUse)пропущенпродолжается
exit code2 (PostToolUse)(уже выполнился) советующий повторпродолжается
exit code2 (Stop)н/дповторяет ход; stderr передаётся модели
exit code0см. выводсм. вывод
exit codeдругойвыполняетсяпродолжается
hookSpecificOutput.permissionDecision"deny" (PreToolUse)пропущенпродолжается
decision"block" (PostToolUse)(уже выполнился) советующий повторпродолжается
decision"block" (Stop)н/дповторяет ход (не более 3 раз); reason показывается пользователю и передаётся модели
continuefalseвыполняетсяостанавливается после этой пачки

SessionStart не блокирует: он никогда не блокирует, не повторяет и не останавливает. Код выхода 2, decision и continue: false игнорируются как управляющие сигналы, чтобы хук никогда не мог помешать сессии открыться. Используются только additionalContext (вставляется в первый ход) и systemMessage (показывается пользователю).

Когда хук PreToolUse отклоняет инструмент: модель получает permissionDecisionReason (или stderr при коде выхода 2) как результат инструмента. Остальные хуки PreToolUse для этого вызова пропускаются.

Когда любой хук задаёт continue: false: все хуки текущей пачки всё равно дорабатывают до конца. Сессия останавливается после. К SessionStart это не относится.

Семантика исполнения ​

Как движок запускает хуки после того, как произошёл вызов инструмента.

Shell ​

Каждая команда хука запускается через системный shell. JSON-ввод подаётся на stdin, хук пишет свой JSON-ответ в stdout. Первая непустая строка stderr используется как запасная причина блокировки, когда хук выходит с кодом 2.

Порядок исполнения ​

  • PreToolUse хуки выполняются последовательно в том порядке, в каком они записаны в settings.json. Исполнение останавливается, как только один хук отклоняет инструмент (через permissionDecision: "deny" или код выхода 2); более поздние хуки того же события не выполняются.
  • PostToolUse хуки выполняются параллельно. Один упавший хук не может отменить другой. Возвращённые результаты сохраняют порядок из settings.json, а не порядок завершения.
  • Stop хуки выполняются параллельно, как и PostToolUse. Matcher в хуках Stop молча игнорируется, потому что у Stop нет инструмента для сопоставления.
  • SessionStart хуки выполняются параллельно в начале сессии, до первого хода. Matcher молча игнорируется, потому что у SessionStart нет инструмента для сопоставления.

Таймауты ​

  • Таймаут по умолчанию — 30 секунд. Переопределяется для каждого хука полем timeout (секунды, максимум 600).
  • По таймауту движок отправляет SIGTERM. Хуки, перехватывающие SIGTERM, получают 5 секунд отсрочки до SIGKILL.

Изоляция ​

Каждый хук срабатывает в своём процессе и со своей копией stdin. Хуки не могут читать stdout или stderr друг друга и не могут передавать друг другу информацию. Когда несколько хуков совпадают с одним вызовом инструмента, их выводы объединяются по правилам матрицы решений; ни один хук не видит результат другого.

Режимы разрешений ​

Поле permission_mode на stdin несёт текущий режим сессии:

ЗначениеОписание
"default"Модель запрашивает разрешение перед каждым инструментом
"auto-accept"Запросы разрешений принимаются автоматически
"plan"Режим планирования. Вызовы инструментов ограничены операциями только для чтения. В режиме планирования хуки пропускаются полностью
"bypass"Запросы пропускаются; обычные проверки безопасности всё равно применяются
"dont-ask"Никогда не спрашивает; всё, что потребовало бы запроса, отклоняется
"" (пусто)Режим не применяется к этому срабатыванию

standard принимается как устаревший псевдоним при установке режима (в settings.json или --permission-mode), но значение, сообщаемое по wire, всегда default.

Режим планирования по замыслу только читает, поэтому защита PreToolUse не нужна и аудит PostToolUse не сработает. Если ваш хук выглядит сломанным, сначала проверьте, не находится ли сессия в режиме планирования.

Пример: полный скрипт хука ​

.commandcode/hooks/guard-shell.sh:

bash
#!/usr/bin/env bash
set -euo pipefail

# Read the entire stdin payload once.
payload=$(cat)

# Common fields on every event.
session_id=$(printf '%s' "$payload" | jq -r '.session_id')
cwd=$(printf '%s' "$payload" | jq -r '.cwd')
event=$(printf '%s' "$payload" | jq -r '.hook_event_name')

# Tool-call fields, present on every tool event.
tool_name=$(printf '%s' "$payload" | jq -r '.tool_name')
tool_display=$(printf '%s' "$payload" | jq -r '.tool_display_name')
cmd=$(printf '%s' "$payload" | jq -r '.tool_input.command // ""')

# Command Code also injects env vars for the same context.
: "${COMMANDCODE_PROJECT_DIR:?}"
: "${COMMANDCODE_SESSION_ID:?}"
: "${COMMANDCODE_HOOK_EVENT:?}"

# Deny a destructive command with a reason the model will see.
if [[ "$cmd" == *"rm -rf /"* ]]; then
  jq -n --arg cmd "$cmd" '{
    continue: true,
    systemMessage: "Blocked destructive command",
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: ("Policy forbids: " + $cmd)
    }
  }'
  exit 0
fi

# Otherwise allow and attach extra context for the model's next turn.
jq -n --arg event "$event" '{
  continue: true,
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "allow",
    additionalContext: ("Verified by guard-shell.sh during " + $event)
  }
}'

Примеры ​

Набор работающих хуков, которые можно скопировать в свой проект. Каждый показывает отдельный сценарий, так что вы можете менять их как угодно. Здесь все они на bash, но хук может быть любой исполняемой программой, разбирающей JSON на stdin.

ПримерСобытиеВидЧто получаете
Блокировка опасных bash-командPreToolUseБлокирующийМодель не может выполнить rm -rf /, curl | sh
Предупреждение о чувствительных файлахPreToolUseВставка контекстаМодель получает указание вычищать секреты перед цитированием
Аудит вызовов инструментовPreToolUse или PostToolUseТолько аудитСовпавшие вызовы добавляются в локальный лог-файл
Контроль качестваStopБлокирующий (повтор)Ассистент не может завершить работу, пока в коде остались маркеры DO NOT SHIP
Вставка git statusSessionStartВставка контекстаМодель знает ветку и несохранённые файлы до начала работы

Каждый пример состоит из двух файлов:

  1. settings.json, который подключает хук.
  2. Shell-скрипт в .commandcode/hooks/<name>.sh.

Убедитесь, что каждый скрипт исполняемый (chmod +x), прежде чем Command Code вызовет хук.

Каждый пример заканчивается блоком Тест с промптом для вставки в Command Code и ожидаемым результатом, чтобы вы могли убедиться, что хук срабатывает.

Блокировка опасных bash-команд ​

Сопоставляет shell-команду модели с коротким списком опасных шаблонов. При совпадении отклоняет инструмент и говорит модели почему, чтобы она не повторяла тот же шаблон.

.commandcode/settings.json:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "shell",
        "hooks": [
          {
            "type": "command",
            "command": "./.commandcode/hooks/deny-dangerous.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

.commandcode/hooks/deny-dangerous.sh:

bash
#!/usr/bin/env bash
set -euo pipefail

cmd=$(jq -r '.tool_input.command // ""')

# Dangerous patterns: rm -rf /, curl | sh, fork bomb, sudo rm
pattern='rm[[:space:]]+-[rR]f?[[:space:]]+/'
pattern+='|curl.*\|.*(sh|bash)'
pattern+='|:\(\)[[:space:]]*\{'
pattern+='|sudo[[:space:]]+rm'

if printf '%s' "$cmd" | grep -qE "$pattern"; then
  jq -n --arg cmd "$cmd" '{
    systemMessage: "blocked dangerous command",
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: ("Command matched a dangerous pattern. Policy forbids: " + ($cmd | .[0:120]))
    }
  }'
else
  exit 0
fi

Тест: блокировка опасных bash-команд:

Промпт в Command Code:

text
Try to run this shell command: rm -rf /tmp/cmd-hook-demo

Ожидаемый результат:

  • заблокировано: команда блокируется до запуска (она совпадает с шаблоном rm -rf /, хотя цель безобидна).
  • Агент видит "Command matched a dangerous pattern. Policy forbids…" и идёт дальше, не повторяя попытку.

Как это работает:

  • Читает tool_input.command из stdin через jq.
  • Сопоставляет с четырьмя шаблонами: rm -rf /, curl | sh, форк-бомба :(){}, sudo rm.
  • При совпадении выдаёт permissionDecision: "deny". Инструмент пропускается, а модель получает permissionDecisionReason как результат инструмента.
  • При промахе выходит с 0 без stdout. Инструмент выполняется как обычно.

Предупреждение о чувствительных файлах при чтении ​

Разрешает любое чтение, но тихо вставляет замечание, когда путь выглядит чувствительным. Модель видит замечание как дополнительный контекст и корректирует ответ.

.commandcode/settings.json:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "read",
        "hooks": [
          {
            "type": "command",
            "command": "./.commandcode/hooks/warn-sensitive-reads.sh"
          }
        ]
      }
    ]
  }
}

.commandcode/hooks/warn-sensitive-reads.sh:

bash
#!/usr/bin/env bash
set -euo pipefail

path=$(jq -r '.tool_input.absolute_path // ""')

# Default note. Upgraded to a strong warning when the path looks sensitive.
ctx="File is being read under an audit hook. Do not paste full contents unless the user asks."
if printf '%s' "$path" | grep -qE '(\.ssh/|/\.env$|\.pem$|id_rsa)'; then
  ctx="SENSITIVE READ: $path. Redact any keys or tokens before quoting from this file."
fi

jq -n --arg ctx "$ctx" '{
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "allow",
    additionalContext: $ctx
  }
}'

Тест: предупреждение о чувствительных файлах:

Промпт в Command Code:

text
Read the .env file in my home directory

Ожидаемый результат:

  • разрешено: чтение разрешено. Хук никогда его не блокирует.
  • Агент получает вставленное замечание "SENSITIVE READ: …/.env. Redact any keys or tokens before quoting from this file." и отказывается вставлять полное содержимое.

Как это работает:

  • Всегда возвращает permissionDecision: "allow". Чтение никогда не блокируется.
  • Отправляет модели additionalContext, который добавляется к результату инструмента перед следующим ходом.
  • Меняет замечание на явное предупреждение о вычищении, когда путь совпадает с .ssh/, .env, .pem или id_rsa.

Аудит вызовов инструментов в лог-файл ​

Пишет каждый совпавший вызов инструмента в append-only файл. Хук ничего не пишет в stdout, поэтому он только наблюдает: инструмент выполняется без изменений. Шаблон (прочитать tool_input через jq, добавить строку с табуляцией) одинаков для любого инструмента. Меняйте событие, matcher и извлекаемое поле под то, что хотите наблюдать.

Используйте PostToolUse, чтобы логировать то, что завершилось. Используйте PreToolUse, чтобы логировать то, что было предпринято; это также ловит команды, которые отклонит более поздний хук, если аудит-хук выполняется раньше.

Записи и правки (PostToolUse) ​

Срабатывает после завершения изменения файла. Использует COMMANDCODE_PROJECT_DIR и COMMANDCODE_SESSION_ID напрямую, поэтому разбирать cwd или session_id из stdin не нужно.

.commandcode/settings.json:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "write|edit",
        "hooks": [
          {
            "type": "command",
            "command": "./.commandcode/hooks/audit-writes.sh"
          }
        ]
      }
    ]
  }
}

.commandcode/hooks/audit-writes.sh:

bash
#!/usr/bin/env bash
set -euo pipefail

LOG_FILE="$COMMANDCODE_PROJECT_DIR/.commandcode/write-audit.log"

# Extract the target file path from the JSON payload on stdin.
path=$(jq -r '.tool_input.file_path // "?"')
timestamp=$(date -u +%FT%TZ)

# Append one tab-separated line per write: timestamp, session ID, file path.
printf '%s\t%s\t%s\n' "$timestamp" "$COMMANDCODE_SESSION_ID" "$path" >> "$LOG_FILE"

Тест: аудит записей:

Промпт в Command Code:

text
Create a new file called config.txt with some test data

Ожидаемый результат:

  • разрешено: файл создан.
  • Хук добавляет одну строку в .commandcode/write-audit.log.
  • Проверьте через cat .commandcode/write-audit.log. Вы должны увидеть строку с табуляцией (время, id сессии, путь файла).

Shell-команды (PreToolUse) ​

Срабатывает до исполнения, поэтому логирует каждую shell-команду, которую выдаёт агент.

.commandcode/settings.json:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "shell",
        "hooks": [
          {
            "type": "command",
            "command": "./.commandcode/hooks/log-shell.sh"
          }
        ]
      }
    ]
  }
}

.commandcode/hooks/log-shell.sh:

bash
#!/usr/bin/env bash
set -euo pipefail

LOG_FILE="/tmp/cmd-shell.log"

# Extract the shell command from the JSON payload on stdin.
command=$(jq -r '.tool_input.command // ""')
timestamp=$(date -u +%FT%TZ)

# Append one tab-separated line to the audit log.
printf '%s\t%s\n' "$timestamp" "$command" >> "$LOG_FILE"

Тест: аудит shell-команд:

Промпт в Command Code:

text
Run this command: ls -la /home
Then tell me you're done.

Ожидаемый результат:

  • разрешено: команда выполняется как обычно.
  • Хук добавляет одну строку в /tmp/cmd-shell.log.
  • Проверьте через cat /tmp/cmd-shell.log. Каждая запись в /tmp/cmd-shell.log имеет вид (поля разделены табуляцией):
text
2026-04-21T17:38:51Z  ls -la /home

Контроль качества (хук Stop) ​

Не даёт агенту завершить ход, пока в коде остаётся маркер DO NOT SHIP. Событие Stop срабатывает в конце хода; этот хук ищет маркер через grep и выходит с кодом 2, когда находит его, отправляя ассистента на ещё один проход с найденными строками как отзывом. Проверка stop_hook_active — канонический шаблон защиты от зацикливания. Без неё хук срабатывал бы бесконечно.

.commandcode/settings.json:

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./.commandcode/hooks/no-ship-gate.sh"
          }
        ]
      }
    ]
  }
}

.commandcode/hooks/no-ship-gate.sh:

bash
#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)

# Already retrying, so let the turn end and don't loop.
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi

# Quality gate: don't finish while "DO NOT SHIP" markers remain.
# On a hit, echo the lines to stderr and exit 2. The engine feeds
# that text to the model on the retry so it knows what to remove.
# `.commandcode` is excluded so the hook can't match its own source.
HITS=$(grep -rn "DO NOT SHIP" . \
  --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.commandcode \
  2>/dev/null || true)

if [ -n "$HITS" ]; then
  echo "Remove these 'DO NOT SHIP' markers before finishing:" >&2
  echo "$HITS" | head -20 >&2
  exit 2
fi

Тест: контроль качества:

Промпт в Command Code:

text
Write a JavaScript function that processes user data.
Include this comment: // DO NOT SHIP - add error handling
Then tell me you're done.

Ожидаемый результат: ход блокируется, агент повторяет сам и завершает работу, когда маркер исчез. По шагам:

  • заблокировано: первый ход: ассистент завершает ответ → хук делает grep → находит маркер DO NOT SHIP → выходит с 2, передав совпавшие строки в stderr. В ленте появляется серая рамка Stop:
text
 Ran 1 stop hook
 └ Stop hook [./.commandcode/hooks/no-ship-gate.sh] exited 2: Remove these 'DO NOT SHIP' markers before finishing: [retry 1/3]
  • Движок передаёт этот текст из stderr модели как сообщение роли user и запускает ход заново.
  • разрешено: второй ход: ассистент убирает маркер и добавляет обработку ошибок. Хук срабатывает снова уже с stop_hook_active: true → выходит с 0. Ход заканчивается.

Если хук блокирует постоянно (например, он сломан), движок ограничивает повторы тремя и завершает ход сообщением Stop hook retry cap reached (3). Ending turn. Same outcome ×4. Fix or disable: ./.commandcode/hooks/no-ship-gate.sh.

Вставка git status на старте сессии ​

Даёт модели фору, явно вставляя текущую git-ветку и несохранённые изменения в первый ход. Событие SessionStart срабатывает один раз в начале сессии (старт, возобновление или очистка). Этот хук печатает состояние репозитория как additionalContext, так что модель видит его ещё до того, как вы наберёте промпт. SessionStart не блокирует, поэтому медленный или падающий хук никогда не задерживает сессию.

.commandcode/settings.json:

json
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./.commandcode/hooks/git-status.sh"
          }
        ]
      }
    ]
  }
}

.commandcode/hooks/git-status.sh:

bash
#!/usr/bin/env bash
set -euo pipefail

# Build a short summary of the repo state.
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
CHANGES=$(git status --porcelain 2>/dev/null | head -20)

SUMMARY="Current git branch: ${BRANCH}."
if [ -n "$CHANGES" ]; then
  SUMMARY="${SUMMARY}"$'\n'"Uncommitted changes:"$'\n'"${CHANGES}"
else
  SUMMARY="${SUMMARY} Working tree is clean."
fi

# Emit additionalContext for the model's first turn.
jq -n --arg ctx "$SUMMARY" '{
  hookSpecificOutput: {
    hookEventName: "SessionStart",
    additionalContext: $ctx
  }
}'

Тест: вставка git status:

Внесите изменение, чтобы в рабочем дереве появились несохранённые правки, затем запустите новую сессию Command Code с промптом:

text
What branch am I on and what have I changed so far?

Ожидаемый результат: модель отвечает названием ветки и списком несохранённых файлов, хотя вы ей об этом не говорили. Контекст приехал в первом ходе из хука. В ленте также появляется серая рамка SessionStart, если хук выдаёт systemMessage.

Лучшие практики ​

Как выбирать хуки вместо других механизмов, писать те, которым можно доверять, тестировать их без запуска Command Code и отлаживать те, что срабатывают не так.

Когда использовать хуки ​

Хуки — правильный инструмент, когда вам нужно детерминированное принуждение вне модели в ваших процессах. Когда использовать хуки, а когда другие возможности Command Code:

МеханизмКогда использовать
ХукиБлокировать разрушительные действия, аудировать каждый вызов инструмента, вставлять контекст, который модель должна увидеть, останавливать сессию по сигналу
МодыРасширять сам Command Code новыми инструментами, командами, флагами или более широким поведением жизненного цикла; см. Моды
НавыкиДать модели процесс, который она может вызвать по своему выбору
Слэш-командыПозволить пользователю вызвать фиксированный промпт или действие
AGENTS.mdОписать нормы проекта, которым модель следует по умолчанию, но от которых может отступить

Как писать безопасные хуки ​

  • Разбирайте stdin через jq -r, никогда через eval. Всё в tool_input пришло от модели и должно считаться недоверенным.
  • Заключайте каждую переменную в кавычки, прежде чем передать её в shell. Пример deny-dangerous.sh использует grep -qE на printf с кавычками и никогда не eval $cmd.
  • Держите timeout небольшим (10 секунд или меньше для синхронных хуков). Медленный хук блокирует вызов инструмента, и Command Code начинает ощущаться тормозным.
  • Предпочитайте additionalContext вместо systemMessage, когда направляете модель, и systemMessage, когда объясняете пользователю нарушение политики.

Тестирование хука локально ​

Чтобы дорабатывать хук, запускать Command Code не нужно. Подайте ему фиктивную нагрузку:

bash
cat <<'EOF' | COMMANDCODE_PROJECT_DIR="$PWD" COMMANDCODE_SESSION_ID=test COMMANDCODE_HOOK_EVENT=PreToolUse ./.commandcode/hooks/deny-dangerous.sh
{
  "session_id": "test",
  "transcript_path": "/tmp/t.jsonl",
  "cwd": ".",
  "hook_event_name": "PreToolUse",
  "permission_mode": "default",
  "tool_name": "shell_command",
  "tool_display_name": "SHELL",
  "tool_input": { "command": "rm -rf /" }
}
EOF

Проверьте stdout (JSON или пусто при «мнения нет») и код выхода (echo $?). Убедитесь, что у скрипта есть права на исполнение (chmod +x), чтобы он мог работать как хук в Command Code.

Частые сбои ​

Что вы увидите, когда хук ведёт себя не так, как ожидалось:

СимптомВероятная причинаРешение
Хук никогда не срабатываетВы в режиме планирования (хуки пропускаются). Regex в matcher не совпадает ни с одним из SHELL, READ, WRITE, EDIT. У скрипта нет прав на исполнениеВыйдите из режима планирования. Сверьте matcher с отображаемыми именами SHELL, READ, WRITE, EDIT. Выполните chmod +x для своих исполняемых файлов.
Инструмент выполняется, несмотря на "deny"permissionDecision написано с ошибкой или отсутствует hookSpecificOutputПроверьте вывод по схеме Вывод хука
В логе ошибки таймаутаХук работает дольше своего timeoutУвеличьте timeout или перенесите медленную работу в фоновый процесс
Хук молча падаетStdout — невалидный JSON при коде выхода 0Возвращайте пустой stdout для «мнения нет» или выдавайте валидный JSON
jq: command not foundjq не установлен на машине, где работает Command Codebrew install jq или перепишите хук на Python или Node

Для всех этих случаев cmd --debug логирует каждую проверку хука (см. ниже).

Отладка хуков ​

Когда хук срабатывает, но ведёт себя не так, как вы ожидаете, запустите Command Code с --debug и смотрите лог:

bash
cmd --debug
# in another terminal
tail -f ~/.commandcode/logs/command.log

Лог записывает каждую проверку хука: проверки доверия, загрузку конфигурации, решения по matcher, нагрузки stdin/stdout и коды выхода, отличные от нуля. Вы можете увидеть, почему хук был вызван (или не был) и что он вернул.

Файл лога существует только пока активен --debug, и дописывается между сессиями, поэтому очищайте его между прогонами, если он шумит.

Производительность ​

Хуки срабатывают на каждом совпавшем вызове инструмента. Медленный хук означает дополнительную задержку на каждом совпавшем вызове.

  • Держите хуки быстрыми. Заметно меньше секунды. Пользователи замечают задержку сразу.
  • Переносите медленную работу в PostToolUse. Он выполняется после завершения инструмента и параллельно, так что медлительность там не блокирует агента.
  • Уводите по-настоящему медленную работу из процесса. Если нужно обратиться к SIEM или серверу политик, отправьте fire-and-forget HTTP-запрос или допишите в локальный лог. Не блокируйтесь на ожидании ответа.

Дальше ​

  • Моды: расширяйте сам Command Code новыми инструментами, командами и более широким поведением жизненного цикла
  • Справочник CLI: флаги и команды, которые сочетаются с хуками
  • Навыки: дайте модели процессы, которые она может вызвать по своему выбору
  • Застряли на пограничном случае схемы? Спросите в нашем сообществе Discord

Источник

Сверено с оригиналом 13 ноября 2026.