Блок hooks: в ~/.hermes/config.yaml, указывающий на сценарии оболочки
CLI + шлюз
Встраиваемые скрипты для блокировки, автоформатирования, внедрения контекста
Все три системы являются неблокирующими — ошибки в любом хуке фиксируются и протоколируются, что никогда не приводит к сбою агента.
Перехватчики событий шлюза
Перехватчики шлюза срабатывают автоматически во время работы шлюза (Telegram, Discord, Slack, WhatsApp, Teams), не блокируя основной конвейер агента.
Создание крючка
Каждый хук представляет собой каталог ~/.hermes/hooks/, содержащий два файла:
~/.hermes/hooks/
└── my-hook/
├── HOOK.yaml # Declares which events to listen for
└── handler.py # Python handler function
КРЮК.yaml
name:my-hookdescription:Log all agent activity to a fileevents:-agent:start-agent:end-agent:step
Список events определяет, какие события запускают ваш обработчик. Вы можете подписаться на любую комбинацию событий, включая подстановочные знаки, такие как command:*.
обработчик.py
importjsonfromdatetimeimportdatetimefrompathlibimportPathLOG_FILE=Path.home()/".hermes"/"hooks"/"my-hook"/"activity.log"asyncdefhandle(event_type:str,context:dict):"""Called for each subscribed event. Must be named 'handle'."""entry={"timestamp":datetime.now().isoformat(),"event":event_type,**context,}withopen(LOG_FILE,"a")asf:f.write(json.dumps(entry)+"\n")
Правила обработчика:
- Должно быть имя handle
- Получает event_type (строку) и context (дикт).
- Может быть async def или обычный def — оба работают
- Ошибки фиксируются и протоколируются, что не приводит к сбою агента.
Обработчики, зарегистрированные для command:*, срабатывают для любого события command: (command:model, command:reset и т. д.). Отслеживайте все слэш-команды с помощью одной подписки.
Примеры
Оповещение Telegram о длинных задачах
Отправьте себе сообщение, когда агент сделает более 10 шагов:
# ~/.hermes/hooks/long-task-alert/HOOK.yamlname:long-task-alertdescription:Alert when agent is taking many stepsevents:-agent:step
# ~/.hermes/hooks/long-task-alert/handler.pyimportosimporthttpxTHRESHOLD=10BOT_TOKEN=os.getenv("TELEGRAM_BOT_TOKEN")CHAT_ID=os.getenv("TELEGRAM_HOME_CHANNEL")asyncdefhandle(event_type:str,context:dict):iteration=context.get("iteration",0)ifiteration==THRESHOLDandBOT_TOKENandCHAT_ID:tools=", ".join(context.get("tool_names",[]))text=f"⚠️ Agent has been running for {iteration} steps. Last tools: {tools}"asyncwithhttpx.AsyncClient()asclient:awaitclient.post(f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",json={"chat_id":CHAT_ID,"text":text},)
Учебное пособие: BOOT.md — запуск контрольного списка при каждой загрузке шлюза
Популярный шаблон сообщества: отправьте контрольный список Markdown по адресу ~/.hermes/BOOT.md и попросите агента запускать его один раз при каждом запуске шлюза. Полезно для «при каждой загрузке проверять сбои cron в ночное время и пинговать меня в Discord, если что-то не удалось», или «подводить итоги развертывания.log за последние 24 часа и публиковать его в Slack #ops».
В этом руководстве показано, как создать его самостоятельно в виде определяемого пользователем хука. Hermes не поставляет встроенный крючок BOOT.md — вы подключаете именно то поведение, которое хотите.
Что мы строим
Файл ~/.hermes/BOOT.md с инструкциями по запуску на естественном языке.
Перехватчик шлюза, который срабатывает на gateway:startup, порождает одноразовый агент с разрешенной моделью/учетными данными вашего шлюза и запускает инструкции BOOT.md.
Соглашение [SILENT], позволяющее агенту отказаться от отправки сообщения, когда сообщать не о чем.
Шаг 1. Напишите свой контрольный список
Создайте ~/.hermes/BOOT.md. Напишите это так, как если бы вы давали инструкции помощнику-человеку:
# Startup Checklist1. Run `hermes cron list` and check if any scheduled jobs failed overnight.
2. If any failed, send a summary to Discord #ops using the `send_message` tool.
3. Check if `/opt/app/deploy.log` has any ERROR lines from the last 24 hours. If yes, summarize them and include in the same Discord message.
4. If nothing went wrong, reply with only `[SILENT]` so no message is sent.
Агент видит это как часть своего приглашения, поэтому все, что вы можете описать простым языком, работает — вызовы инструментов, команды оболочки, отправка сообщений, суммирование файлов.
name:boot-mddescription:Run ~/.hermes/BOOT.md on gateway startupevents:-gateway:startup
~/.hermes/hooks/boot-md/handler.py
"""Run ~/.hermes/BOOT.md on every gateway startup."""importloggingimportthreadingfrompathlibimportPathlogger=logging.getLogger("hooks.boot-md")BOOT_FILE=Path.home()/".hermes"/"BOOT.md"def_build_prompt(content:str)->str:return("You are running a startup boot checklist. Follow the instructions ""below exactly.\n\n""---\n"f"{content}\n""---\n\n""Execute each instruction. Use the send_message tool to deliver any ""messages to platforms like Discord or Slack.\n""If nothing needs attention and there is nothing to report, reply ""with ONLY: [SILENT]")def_run_boot_agent(content:str)->None:"""Spawn a one-shot agent and execute the checklist. Uses the gateway's resolved model and runtime credentials so this works against custom endpoints, aggregators, and OAuth-based providers alike. """try:fromgateway.runimport_resolve_gateway_model,_resolve_runtime_agent_kwargsfromrun_agentimportAIAgentagent=AIAgent(model=_resolve_gateway_model(),**_resolve_runtime_agent_kwargs(),platform="gateway",quiet_mode=True,skip_context_files=True,skip_memory=True,max_iterations=20,)result=agent.run_conversation(_build_prompt(content))response=result.get("final_response","")ifresponseand"[SILENT]"notinresponse:logger.info("boot-md completed: %s",response[:200])else:logger.info("boot-md completed (nothing to report)")exceptExceptionase:logger.error("boot-md agent failed: %s",e)asyncdefhandle(event_type:str,context:dict)->None:ifnotBOOT_FILE.exists():returncontent=BOOT_FILE.read_text(encoding="utf-8").strip()ifnotcontent:returnlogger.info("Running BOOT.md (%d chars)",len(content))# Background thread so gateway startup isn't blocked on a full agent turn.thread=threading.Thread(target=_run_boot_agent,args=(content,),name="boot-md",daemon=True,)thread.start()
Две ключевые линии:
_resolve_gateway_model() считывает текущую конфигурируемую модель шлюза.
_resolve_runtime_agent_kwargs() разрешает учетные данные поставщика так же, как это делает обычный шлюз, включая ключи API, базовые URL-адреса, токены OAuth и пулы учетных данных.
Без них простой AIAgent() возвращается к встроенным настройкам по умолчанию и выдает ошибку 401 для любой конечной точки, отличной от умолчанию.
Шаг 3: Проверьте это
Перезапустите шлюз:
hermesgatewayrestart
Смотрите логи:
hermeslogs--follow--levelINFO|grepboot-md
Вы должны увидеть Running BOOT.md (N chars), за которым следует либо boot-md completed: ... (сводка того, что сделал агент), либо boot-md completed (nothing to report), когда агент ответил [SILENT].
Удалите ~/.hermes/BOOT.md, чтобы отключить контрольный список — перехватчик остается загруженным, но автоматически пропускает его, когда файла нет.
Расширение шаблона
Контрольные списки с учетом расписания: выключите datetime.now().weekday() в инструкциях BOOT.md («если сегодня понедельник, проверьте также еженедельный журнал развертывания»). Инструкции представляют собой текст в свободной форме, поэтому все, о чем агент может рассуждать, является честной игрой.
Несколько контрольных списков: укажите перехватчик на другой файл (STARTUP.md, MORNING.md и т. д.) и зарегистрируйте для каждого отдельные каталоги перехватчиков.
Вариант без агента: если вам не нужен полный цикл агента, полностью пропустите AIAgent и попросите обработчик отправить фиксированное уведомление непосредственно через httpx. Дешевле, быстрее и не зависит от поставщика.
Почему это не встроенный
Более ранняя версия Hermes поставляла это как встроенный перехватчик и автоматически создавала агент с пустыми настройками по умолчанию при каждой загрузке шлюза. Это удивило пользователей с настраиваемыми конечными точками и сделало эту функцию невидимой для пользователей, которые не знали, что она работает. Сохранение его в виде документированного шаблона, созданного вами в каталоге хуков, означает, что вы точно видите, что он делает, и соглашаетесь на него, записывая файлы.
Как это работает
При запуске шлюза HookRegistry.discover_and_load() сканирует ~/.hermes/hooks/.
Каждый подкаталог с HOOK.yaml + handler.py загружается динамически.
Обработчики регистрируются на заявленные ими события.
В каждой точке жизненного цикла hooks.emit() запускает все соответствующие обработчики.
Ошибки в любом обработчике фиксируются и протоколируются — сломанный хук никогда не приводит к сбою агента.
:::информация
Перехватчики шлюза срабатывают только на шлюзе (Telegram, Discord, Slack, WhatsApp, Teams). CLI не загружает перехватчики шлюза. Для хуков, которые работают везде, используйте хуки плагинов.
Хуки плагинов
Плагины могут регистрировать перехватчики, которые срабатывают в сеансах как CLI, так и шлюза. Они регистрируются программно через ctx.register_hook() в функции register() вашего плагина.
– Обратные вызовы получают аргументы ключевых слов. Всегда принимайте **kwargs для совместимости — новые параметры могут быть добавлены в будущих версиях без нарушения работы вашего плагина.
- Если обратный вызов сбой, он протоколируется и пропускается. Другие перехватчики и агент продолжают работать нормально. Некорректно работающий плагин никогда не сможет сломать агент.
- Возвращаемые значения двух перехватчиков влияют на поведение: pre_tool_call может блокировать инструмент, а pre_llm_call может вводить контекст в вызов LLM. Все остальные крючки являются наблюдателями по принципу «выстрелил и забыл».
Имя запускаемого инструмента (например, "terminal", "web_search", "read_file")
args
dict
Аргументы, переданные моделью инструменту
task_id
str
Идентификатор сеанса/задачи. Пустая строка, если не установлена.
Срабатывает: В model_tools.py, внутри handle_function_call(), перед запуском обработчика инструмента. Срабатывает один раз за вызов инструмента — если модель вызывает 3 инструмента параллельно, это срабатывает 3 раза.
Возвращаемое значение — наложить вето на вызов:
return{"action":"block","message":"Reason the tool call was blocked"}
Агент замыкает инструмент с помощью message, поскольку в модель возвращается ошибка. Выигрывает первая соответствующая директива блока (сначала регистрируются плагины Python, затем перехватчики оболочки). Любое другое возвращаемое значение игнорируется, поэтому существующие обратные вызовы только для наблюдателей продолжают работать без изменений.
Случаи использования: ведение журналов, журналы аудита, счетчики вызовов инструментов, блокировка опасных операций, ограничение скорости, применение политик для каждого пользователя.
Возвращаемое значение инструмента (всегда строка JSON)
task_id
str
Идентификатор сеанса/задачи. Пустая строка, если не установлена.
duration_ms
int
Время, которое заняла отправка инструмента, в миллисекундах (измеряется с помощью time.monotonic() около registry.dispatch()).
Срабатывает: В model_tools.py, внутри handle_function_call(), после возврата обработчика инструмента. Срабатывает один раз за вызов инструмента. Не срабатывает, если инструмент вызвал необработанное исключение (вместо этого ошибка перехватывается и возвращается в виде строки ошибки JSON, а post_tool_call срабатывает с этой строкой ошибки как result).
Возвращаемое значение: игнорируется.
Случаи использования. Регистрация результатов инструментов, сбор показателей, отслеживание показателей успеха/неуспехов инструментов, информационные панели о задержках, оповещения о бюджете для каждого инструмента, отправка уведомлений о завершении работы определенных инструментов.
Пример: отслеживание показателей использования инструмента:
Срабатывает один раз за ход, до начала цикла вызова инструмента. Это единственный хук, для которого используется возвращаемое значение — он может вставлять контекст в пользовательское сообщение текущего хода.
Исходное сообщение пользователя на этот ход (до применения каких-либо навыков)
conversation_history
list
Копия полного списка сообщений (формат OpenAI: [{"role": "user", "content": "..."}])
is_first_turn
bool
True, если это первый ход новой сессии, False при последующих ходах
model
str
Идентификатор модели (например, "anthropic/claude-sonnet-4.6")
platform
str
Где запущен сеанс: "cli", "telegram", "discord" и т. д.
Срабатывает: В run_agent.py, внутри run_conversation(), после сжатия контекста, но перед основным циклом while. Срабатывает один раз за вызов run_conversation() (т. е. один раз за ход пользователя), а не один раз за вызов API в цикле инструмента.
Возвращаемое значение: Если обратный вызов возвращает dict с ключом "context" или простую непустую строку, текст добавляется к сообщению пользователя текущего хода. Верните None для отсутствия инъекции.
# Inject contextreturn{"context":"Recalled memories:\n- User likes Python\n- Working on hermes-agent"}# Plain string (equivalent)return"Recalled memories:\n- User likes Python"# No injectionreturnNone
При внедрении контекста: Всегда сообщение пользователя, а не системное приглашение. Это сохраняет кеш подсказок — системные подсказки остаются одинаковыми на протяжении всего хода, поэтому кэшированные жетоны используются повторно. Системная подсказка — территория Гермеса (наведение модели, применение инструментов, личность, навыки). Плагины вносят контекст вместе с вводом пользователя.
Весь внедренный контекст является эфемерным — добавляется только во время вызова API. Исходное сообщение пользователя в истории разговоров никогда не изменяется, и ничего не сохраняется в базе данных сеанса.
Когда несколько плагинов возвращают контекст, их выходные данные объединяются двойными символами новой строки в порядке обнаружения плагинов (в алфавитном порядке по имени каталога).
Случаи использования: вызов памяти, внедрение контекста RAG, ограждения, пошаговая аналитика.
POLICY="Never execute commands that delete files without explicit user confirmation."defguardrails(**kwargs):return{"context":POLICY}defregister(ctx):ctx.register_hook("pre_llm_call",guardrails)
post_llm_call
Срабатывает один раз за ход, после завершения цикла вызова инструмента и получения окончательного ответа агентом. Срабатывает только при успешных поворотах — не срабатывает, если ход был прерван.
Копия полного списка сообщений после завершения хода
model
str
Идентификатор модели
platform
str
Где проходит сеанс
Срабатывает: В run_agent.py, внутри run_conversation(), после завершения цикла инструмента с окончательным ответом. Защищено if final_response and not interrupted — поэтому он не срабатывает, когда пользователь прерывает операцию в середине хода или когда агент достигает предела итерации, не выдавая ответа.
Возвращаемое значение: игнорируется.
Случаи использования: синхронизация данных разговоров с внешней системой памяти, вычисление показателей качества ответов, протоколирование сводок ходов, запуск последующих действий.
Срабатывает один раз при создании нового сеанса. Не срабатывает при продолжении сеанса (когда пользователь отправляет второе сообщение в существующем сеансе).
Срабатывает: В run_agent.py, внутри run_conversation(), во время первого поворота нового сеанса — особенно после построения системного приглашения, но до запуска цикла инструмента. Проверка if not conversation_history (нет предыдущих сообщений = новый сеанс).
Возвращаемое значение: игнорируется.
Примеры использования: Инициализация состояния на уровне сеанса, разогрев кешей, регистрация сеанса во внешней службе, запуск сеанса регистрации.
Срабатывает в самом конце каждого вызова run_conversation(), независимо от результата. Также срабатывает из обработчика выхода CLI, если агент находился в середине хода, когда пользователь вышел.
True, если агент выдал окончательный ответ, False в противном случае
interrupted
bool
True, если ход был прерван (пользователь отправил новое сообщение, /stop или вышел)
model
str
Идентификатор модели
platform
str
Где проходит сеанс
Пожары: В двух местах:
1. run_agent.py — в конце каждого вызова run_conversation(), после очистки. Всегда срабатывает, даже если ход ошибочный.
2. cli.py — в обработчике atexit CLI, но только если агент находился в середине хода (_agent_running=True), когда произошел выход. Это перехватывает Ctrl+C и /exit во время обработки. В данном случае completed=False и interrupted=True.
Возвращаемое значение: игнорируется.
Случаи использования: очистка буферов, закрытие соединений, сохранение состояния сеанса, регистрация продолжительности сеанса, очистка ресурсов, инициализированных в on_session_start.
Пример — промывка и очистка:
_session_caches={}defcleanup_session(session_id,completed,interrupted,**kwargs):cache=_session_caches.pop(session_id,None)ifcache:# Flush accumulated data to disk or external servicestatus="completed"ifcompletedelse("interrupted"ifinterruptedelse"failed")print(f"Session {session_id} ended: {status}, {cache['tool_calls']} tool calls")defregister(ctx):ctx.register_hook("on_session_end",cleanup_session)
Срабатывает, когда CLI или шлюз обрывает активный сеанс — например, когда пользователь запускает /new, сборщик мусора шлюза завершает сеанс бездействия или CLI завершает работу с активным агентом. Это последний шанс сбросить состояние, связанное с исходящим сеансом, прежде чем его личность исчезнет.
Идентификатор исходящего сеанса. Может быть None, если активного сеанса не существовало.
platform
str
"cli" или название платформы обмена сообщениями ("telegram", "discord" и т. д.).
Срабатывает: В cli.py (при выходе /new/CLI) и gateway/run.py (когда сеанс сбрасывается или выполняется сборщик мусора). Всегда в паре с on_session_reset на стороне шлюза.
Возвращаемое значение: игнорируется.
Случаи использования. Сохранение окончательных показателей сеанса до того, как идентификатор сеанса будет удален, закрытие ресурсов каждого сеанса, создание окончательного события телеметрии, удаление операций записи в очереди.
on_session_reset
Срабатывает, когда шлюз заменяет новый сеансовый ключ для активного чата — пользователь вызвал /new, /reset, /clear или адаптер выбрал новый сеанс после окна простоя. Это позволяет плагинам реагировать на факт очистки состояния диалога, не дожидаясь следующего on_session_start.
Идентификатор нового сеанса (уже изменен на новое значение).
platform
str
Название платформы обмена сообщениями.
Срабатывает: В gateway/run.py, сразу после выделения нового сеансового ключа, но до обработки следующего входящего сообщения. На шлюзе порядок следующий: on_session_finalize(old_id) → обмен → on_session_reset(new_id) → on_session_start(new_id) на первом входящем обороте.
Возвращаемое значение: игнорируется.
Примеры использования: сброс кешей каждого сеанса с ключом session_id, создание аналитики с ротацией сеансов, заполнение сегмента нового состояния.
См. Руководство по созданию плагина для получения полного пошагового руководства, включая схемы инструментов, обработчики и расширенные шаблоны перехватчиков.
subagent_stop
Срабатывает один раз для каждого дочернего агента после завершения delegate_task. Независимо от того, делегировали ли вы одну задачу или пакет из трех, этот крючок срабатывает один раз для каждого дочернего процесса, сериализуемого в родительском потоке.
Тег роли оркестратора установлен для дочернего элемента (None, если эта функция не включена)
child_summary
str \| None
Окончательный ответ, который ребенок возвращает родителю
child_status
str
"completed", "failed", "interrupted" или "error"
duration_ms
int
Время, потраченное настенными часами на бег ребенка, в миллисекундах
Срабатывает: В tools/delegate_tool.py, после ThreadPoolExecutor.as_completed() истощаются все дочерние фьючерсы. Запуск передается родительскому потоку, поэтому авторам хуков не нужно думать о параллельном выполнении обратного вызова.
Возвращаемое значение: игнорируется.
Случаи использования: ведение журнала действий по оркестрации, накопление дочерней длительности для выставления счетов, запись записей аудита после делегирования.
:::информация
При интенсивном делегировании (например, роли оркестратора × 5 листьев × глубина вложенности) subagent_stop срабатывает много раз за ход. Обеспечьте быстрый обратный вызов; перенести дорогостоящую работу в фоновую очередь.
pre_gateway_dispatch
Срабатывает один раз для каждого входящего MessageEvent на шлюзе, после защиты внутренних событий, но перед аутентификацией/сопряжением и отправкой агента. Это точка перехвата для политик потока сообщений на уровне шлюза (окна только для прослушивания, передача вручную, маршрутизация для каждого чата и т. д.), которые не вписываются ни в один адаптер отдельной платформы.
Нормализованное входящее сообщение (имеет .text, .source, .message_id, .internal и т. д.).
gateway
GatewayRunner
Активный шлюз, поэтому плагины могут вызывать gateway.adapters[platform].send(...) для ответов по побочному каналу (уведомления владельца и т. д.).
session_store
SessionStore
Для автоматического приема стенограмм через session_store.append_to_transcript(...).
Срабатывает: В gateway/run.py, внутри GatewayRunner._handle_message(), сразу после вычисления is_internal. Внутренние события полностью пропускают перехват (они генерируются системой (завершения фоновых процессов и т. д.) и не должны контролироваться политикой, ориентированной на пользователя).
Возвращаемое значение:None или dict. Побеждает первое признанное действие; остальные результаты плагина игнорируются. Исключения в обратных вызовах плагинов перехватываются и протоколируются; шлюз всегда не переходит к нормальной отправке в случае ошибки.
Вернуться
Эффект
{"action": "skip", "reason": "..."}
Отбросьте сообщение — ни ответа агента, ни процесса сопряжения, ни аутентификации. Предполагается, что плагин обработал это (например, автоматически включил в расшифровку).
{"action": "rewrite", "text": "new text"}
Замените event.text, затем продолжите обычную отправку с измененным событием. Полезно для объединения буферизованных внешних сообщений в одно приглашение.
{"action": "allow"} / None
Обычная отправка — запускает полную цепочку аутентификации/спаривания/агентского цикла.
Случаи использования: групповые чаты только для прослушивания (отвечают только при пометке; буферизуют окружающие сообщения в контекст); передача сообщений человеком (бесшумная обработка сообщений клиента, в то время как владелец обрабатывает чат вручную); ограничение скорости по профилям; маршрутизация на основе политик.
Пример: отключите неавторизованные DM в автоматическом режиме, не активируя код сопряжения:
Срабатывает непосредственно перед показом пользователю запроса на одобрение — охватывает все поверхности: интерактивный интерфейс командной строки, TUI Ink, платформы шлюзов (Telegram, Discord, Slack, WhatsApp, Matrix и т. д.) и клиенты ACP (VS Code, Zed, JetBrains).
Это подходящее место для подключения настраиваемого уведомления — например, приложения в строке меню macOS, которое выводит уведомление о разрешении/запрете, или журнала аудита, в котором записывается каждый запрос на утверждение с контекстом.
Понятно понятные причины, по которым команда помечена (объединяется при совпадении нескольких шаблонов)
pattern_key
str
Первичный графический ключ, вызвавший утверждение (например, "rm_rf", "sudo")
pattern_keys
list[str]
Все совпавшие графические ключи
session_key
str
Идентификатор сеанса, полезный для определения области уведомлений для каждого чата
surface
str
"cli" для интерактивных подсказок CLI/TUI, "gateway" для утверждений асинхронной платформы
Возвращаемое значение: игнорируется. Крючки здесь доступны только наблюдателю; они не могут наложить вето или предварительно ответить на утверждение. Используйте pre_tool_call, чтобы заблокировать инструмент до того, как он достигнет системы утверждения.
Случаи использования: уведомления на рабочем столе, push-уведомления, ведение журнала аудита, веб-перехватчики Slack, маршрутизация эскалации, метрики.
Пример — уведомление на рабочем столе в macOS:
importsubprocessdefnotify_approval(command,description,session_key,**kwargs):title="Hermes needs approval"body=f"{description}: {command[:80]}"subprocess.Popen(["osascript","-e",f'display notification "{body}" with title "{title}"',])defregister(ctx):ctx.register_hook("pre_approval_request",notify_approval)
post_approval_response
Срабатывает после того, как пользователь отвечает на запрос на одобрение (или время ожидания запроса истекает).
Один из "once", "session", "always", "deny" или "timeout"
Возвращаемое значение: игнорируется.
Случаи использования: закройте соответствующее уведомление на рабочем столе, запишите окончательное решение в журнал аудита, обновите показатели, настройте ограничитель скорости.
deflog_decision(command,choice,session_key,**kwargs):logger.info("approval %s: %s for session %s",choice,command[:60],session_key)defregister(ctx):ctx.register_hook("post_approval_response",log_decision)
transform_tool_result
Срабатывает после возврата инструмента и до добавления результата к диалогу. Позволяет плагину перезаписать строку результата ЛЮБОГО инструмента, а не только вывод терминала, прежде чем модель увидит ее.
Инструмент, выдавший результат (read_file, web_extract, delegate_task, …).
arguments
dict
Аргументы, с которыми модель вызывает инструмент.
result
str
Необработанная строка результата инструмента, после усечения и после ANSI-полосы.
task_id
str \| None
Идентификатор задачи/сеанса при работе в средах RL/тестирования.
Возвращаемое значение:str для замены результата (возвращенная строка — это то, что видит модель), None для того, чтобы оставить ее без изменений.
Случаи использования: Редактируйте персональные данные организации из выходных данных web_extract, помещайте длинные ответы инструмента JSON в сводный заголовок, добавляйте подсказки, дополненные поиском, в результаты read_file, переписывайте отчеты субагента delegate_task в схему, специфичную для проекта.
Применяется к каждому инструменту. О переписывании только для терминала см. transform_terminal_output ниже — оно уже и выполняется на более ранней стадии конвейера (до усечения, перед редактированием).
transform_terminal_output
Срабатывает внутри конвейера вывода переднего плана инструмента terminal, до усечения 50 КБ по умолчанию, удаления ANSI и секретного редактирования. Позволяет плагинам перезаписывать необработанный стандартный вывод/stderr команды оболочки до того, как его затронет какая-либо последующая обработка.
Необработанный комбинированный поток stdout/stderr (может быть очень большим — усечение происходит после перехвата).
exit_code
int
Код завершения процесса.
cwd
str
Рабочий каталог, в котором выполнялась команда.
Возвращаемое значение:str, чтобы заменить вывод, None, чтобы оставить его без изменений.
Случаи использования: внедряйте сводные данные для команд, которые производят массивный вывод (du -ah, find, tree), помечайте выходные данные маркером, специфичным для проекта, чтобы последующие перехватчики знали, как с ними обращаться, удаляйте временной шум, который меняется между запусками и блокирует кэширование подсказок.
defsummarize_find(command,output,**kwargs):ifcommand.startswith("find ")andlen(output)>50_000:lines=output.count("\n")head="\n".join(output.splitlines()[:40])returnf"{head}\n\n[summary: {lines} paths total, showing first 40]"returnNonedefregister(ctx):ctx.register_hook("transform_terminal_output",summarize_find)
Хорошо сочетается с transform_tool_result (который охватывает все остальные инструменты).
Крючки-ракушки
Объявите перехватчики сценариев оболочки в своем cli-config.yaml, и Hermes будет запускать их как подпроцессы всякий раз, когда срабатывает соответствующее событие перехватчика плагина — как в сеансах CLI, так и в сеансах шлюза. Разработка плагинов Python не требуется.
Используйте перехватчики оболочки, если вы хотите, чтобы однофайловый скрипт (Bash, Python, что-нибудь с шебангом) выполнял следующие действия:
Блокировать вызов инструмента — отклонять опасные команды terminal, применять политики для каждого каталога, требовать одобрения для деструктивных операций write_file / patch.
Запускать после вызова инструмента — автоматически форматировать файлы Python или TypeScript, которые только что написал агент, регистрировать вызовы API, запускать рабочий процесс CI.
Внедрить контекст в следующий ход LLM — добавьте к сообщению пользователя выходные данные git status, текущий день недели или полученные документы (см. pre_llm_call).
Наблюдение за событиями жизненного цикла – записывайте строку журнала, когда субагент завершает работу (subagent_stop) или начинает сеанс (on_session_start).
Перехватчики оболочки регистрируются путем вызова agent.shell_hooks.register_from_config(cfg) как при запуске CLI (hermes_cli/main.py), так и при запуске шлюза (gateway/run.py). Они естественным образом компонуются с помощью плагинов Python — оба проходят через один и тот же диспетчер.
Жизненный цикл шлюза (gateway:startup, agent:*, command:*)
Можно заблокировать вызов инструмента
Да (pre_tool_call)
Да (pre_tool_call)
Нет
Может внедрить контекст LLM
Да (pre_llm_call)
Да (pre_llm_call)
Нет
Согласие
Подсказка при первом использовании для каждой пары (event, command)
Неявное (доверие к плагину Python)
Неявное (директор доверия)
Межпроцессная изоляция
Да (подпроцесс)
Нет (в процессе)
Нет (в процессе)
Схема конфигурации
hooks:<event_name>:# Must be in VALID_HOOKS-matcher:"<regex>"# Optional; used for pre/post_tool_call onlycommand:"<shellcommand>"# Required; runs via shlex.split, shell=Falsetimeout:<seconds># Optional; default 60, capped at 300hooks_auto_accept:false# See "Consent model" below
Имена событий должны быть одним из событий перехвата плагина; опечатки приводят к ответу «Вы имели в виду X?» предупреждение и пропускаются. Неизвестные ключи внутри одной записи игнорируются; отсутствие command — это пропуск с предупреждением. timeout > 300 зажат с предупреждением.
Протокол передачи данных JSON
Каждый раз при возникновении события Hermes запускает подпроцесс для каждого совпадающего перехватчика (если позволяет сопоставитель), передает полезную нагрузку JSON на stdin и считывает stdout обратно как JSON.
stdin — полезная нагрузка, которую получает скрипт:
tool_name и tool_input — это null для событий, не связанных с инструментами (pre_llm_call, subagent_stop, жизненный цикл сеанса). Дикт extra содержит все кварги, специфичные для событий (user_message, conversation_history, child_role, duration_ms, …). Несериализуемые значения преобразуются в строки, а не опускаются.
stdout — необязательный ответ:
//Blockapre_tool_call(bothshapesaccepted;normalisedinternally):{"decision":"block","reason":"Forbidden: rm -rf"}//Claude-Codestyle{"action":"block","message":"Forbidden: rm -rf"}//Hermes-canonical//Injectcontextforpre_llm_call:{"context":"Today is Friday, 2026-04-17"}//Silentno-op—anyempty/non-matchingoutputisfine:
Неверный формат JSON, ненулевые коды выхода и тайм-ауты регистрируют предупреждение, но никогда не прерывают цикл агента.
Рабочие примеры
1. Автоматическое форматирование файлов Python после каждой записи.
Контекстное представление файла агентом не перечитывается автоматически — переформатирование влияет только на файл на диске. Последующие вызовы read_file подхватывают отформатированную версию.
Событие UserPromptSubmit в Claude Code намеренно не является отдельным событием Hermes — pre_llm_call срабатывает в том же месте и уже поддерживает внедрение контекста. Используйте его здесь.
Каждая уникальная пара (event, command) запрашивает у пользователя подтверждение при первом ее просмотре Hermes, а затем сохраняет решение ~/.hermes/shell-hooks-allowlist.json. Последующие запуски (CLI или шлюз) пропускают запрос.
Три аварийных люка обходят интерактивную подсказку — достаточно любого:
Флаг --accept-hooks в CLI (например, hermes --accept-hooks chat)
Переменная среды HERMES_ACCEPT_HOOKS=1
hooks_auto_accept: true в cli-config.yaml
Для запусков без TTY (шлюз, cron, CI) требуется один из этих трех — в противном случае любой вновь добавленный хук останется незарегистрированным и зарегистрирует предупреждение.
Редактированию скриптов доверяют без уведомления. Ключи белого списка указаны в точной командной строке, а не в хэше скрипта, поэтому редактирование скрипта на диске не делает согласие недействительным. hermes hooks doctor помечает отклонение времени, чтобы вы могли заметить изменения и решить, следует ли их повторно утверждать.
Интерфейс командной строки hermes hooks
Команда
Что он делает
hermes hooks list
Дамп настроенных перехватчиков со статусом сопоставления, тайм-аутом и согласием
hermes hooks test <event> [--for-tool X] [--payload-file F]
Запустите каждый соответствующий крючок против синтетической полезной нагрузки и распечатайте проанализированный ответ
hermes hooks revoke <command>
Удалить все записи белого списка, соответствующие <command> (вступает в силу при следующем перезапуске)
hermes hooks doctor
Для каждого настроенного перехватчика: проверьте бит выполнения, состояние списка разрешений, отклонение времени mtime, достоверность вывода JSON и приблизительное время выполнения
Безопасность
Перехватчики оболочки запускаются с использованием ваших полных учетных данных пользователя — та же граница доверия, что и запись cron или псевдоним оболочки. Рассматривайте блок hooks: в config.yaml как привилегированную конфигурацию:
Только справочные сценарии, которые вы написали или полностью просмотрели.
Храните сценарии внутри ~/.hermes/agent-hooks/, чтобы путь можно было легко проверить.
Повторно запустите hermes hooks doctor после получения общей конфигурации, чтобы обнаружить вновь добавленные перехватчики до их регистрации.
Если ваш файл config.yaml контролируется всей командой, просмотрите PR, которые изменяют раздел hooks:, так же, как вы просматриваете конфигурацию CI.
Порядок и приоритет
Как хуки плагинов Python, так и хуки оболочки проходят через один и тот же диспетчер invoke_hook(). Плагины Python регистрируются первыми (discover_and_load()), вторыми являются перехватчики оболочки (register_from_config()), поэтому решения о блоках Python pre_tool_call имеют приоритет в случае равенства. Выигрывает первый действительный блок — агрегатор возвращается, как только какой-либо обратный вызов генерирует {"action": "block", "message": str} с непустым сообщением.