событий Хуки

Гермес имеет три системы хуков, которые запускают пользовательский код в ключевых точках жизненного цикла:

Система Регистрируется через Выполняется в Вариант использования
Хуки шлюза HOOK.yaml + handler.py в ~/.hermes/hooks/ Только в шлюзе Логирование, оповещения, вебхуки
Хуки плагинов ctx.register_hook() в плагине CLI + шлюз Инструменты для захвата, метрики, ограничители
Шелл-хуки заблокировать hooks: в ~/.hermes/config.yaml, указанный на шелл-скриптах CLI + шлюз Подключаемые скрипты для блокировки, автоформатирования, контекста обслуживания

Все три системы неблокирующие — ошибки в любом хуке перехватываются и регистрируются, никогда не приводят к сбою агента.

Хуки шлюза (перехватчики событий шлюза)

Хуки шлюза отключаются автоматически во время работы шлюза (Telegram, Discord, Slack, WhatsApp, Teams), не блокируя основной конвейер агента.

Создание хука

Каждый хук представляет собой каталог в ~/.hermes/hooks/, состоящий из двух файлов:

~/.hermes/hooks/
└── my-hook/
    ├── HOOK.yaml      # Объявляет, какие события прослушивать
    └── handler.py     # Функция-обработчик на Python

КРЮК.yaml

name: my-hook
description: Логировать всю активность агента в файл
events:
  - agent:start
  - agent:end
  - agent:step

Список событий определяет, какие события активируют вашего обработчика. Вы можете подписаться на любые события, включая шаблоны вроде command:*.

обработчик.py

import json
from datetime import datetime
from pathlib import Path

LOG_FILE = Path.home() / ".hermes" / "hooks" / "my-hook" / "activity.log"

async def handle(event_type: str, context: dict):
    """Вызывается для каждого подписанного события. Имя функции должно быть 'handle'."""
    entry = {
        "timestamp": datetime.now().isoformat(),
        "event": event_type,
        **context,
    }
    with open(LOG_FILE, "a") as f:
        f.write(json.dumps(entry) + "\n")

Правила для обработчиков: - Должен называться ручка - Получает event_type (строка) и context (словарь) - Может быть async def или обычный def — оба работают - Ошибки перехватываются и регистрируются, никогда не приводя к сбою агента

Доступные события

Событие Когда реализация Ключи контекста
шлюз: запуск Запуск процесса шлюза платформы (список активных платформ)
сеанс: начало Создана новая сессия обмена сообщениями платформа, user_id, session_id, session_key
сеанс: конец Сессия завершена (до сброса) платформа, user_id, session_key
сеанс: сброс Пользователь выполнил /new или /reset платформа, user_id, session_key
агент: старт Агент начала обработки сообщений платформа, user_id, session_id, message
агент:шаг каждая итерация цикла вызывает инструменты платформа, user_id, session_id, iteration, tool_names
агент:конец Агент завершает обработку платформа, user_id, session_id, сообщение, ответ
команда:* Выполнена любая слэш-команда платформа, user_id, команда, args

Соответствие по шаблону

Обработчики, зарегистрированные для command:*, срабатывают для любых событий command: (command:model, command:reset и т.д.). Мониторинг всех слэш-команд с одной подпиской.

Примеры

Оповещение в Telegram о длительных задачах

Отправьте себе сообщение, когда агент выполнит более 10 шагов:

# ~/.hermes/hooks/long-task-alert/HOOK.yaml
name: long-task-alert
description: Оповещение, когда агент выполняет много шагов
events:
  - agent:step
# ~/.hermes/hooks/long-task-alert/handler.py
import os
import httpx

THRESHOLD = 10
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
CHAT_ID = os.getenv("TELEGRAM_HOME_CHANNEL")

async def handle(event_type: str, context: dict):
    iteration = context.get("iteration", 0)
    if iteration == THRESHOLD and BOT_TOKEN and CHAT_ID:
        tools = ", ".join(context.get("tool_names", []))
        text = f"⚠️ Агент работает уже {iteration} шагов. Последние инструменты: {tools}"
        async with httpx.AsyncClient() as client:
            await client.post(
                f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
                json={"chat_id": CHAT_ID, "text": text},
            )

Логирование использования команды

Отслеживайте, какие слэш-команды использовались:

# ~/.hermes/hooks/command-logger/HOOK.yaml
name: command-logger
description: Логирование использования слэш-команд
events:
  - command:*
# ~/.hermes/hooks/command-logger/handler.py
import json
from datetime import datetime
from pathlib import Path

LOG = Path.home() / ".hermes" / "logs" / "command_usage.jsonl"

def handle(event_type: str, context: dict):
    LOG.parent.mkdir(parents=True, exist_ok=True)
    entry = {
        "ts": datetime.now().isoformat(),
        "command": context.get("command"),
        "args": context.get("args"),
        "platform": context.get("platform"),
        "user": context.get("user_id"),
    }
    with open(LOG, "a") as f:
        f.write(json.dumps(entry) + "\n")

Вебхук на старте сессии

Отправка POST во внешний сервис при создании новой сессии:

# ~/.hermes/hooks/session-webhook/HOOK.yaml
name: session-webhook
description: Уведомлять внешний сервис о новых сессиях
events:
  - session:start
  - session:reset
# ~/.hermes/hooks/session-webhook/handler.py
import httpx

WEBHOOK_URL = "https://your-service.example.com/hermes-events"

async def handle(event_type: str, context: dict):
    async with httpx.AsyncClient() as client:
        await client.post(WEBHOOK_URL, json={
            "event": event_type,
            **context,
        }, timeout=5)

Учебное пособие: BOOT.md — Запуск стартового чек-листа при каждом компоненте шлюза

Популярный шаблон в сообществе: положите markdown-check-list в ~/.hermes/BOOT.md, и агент будет выполнять его один раз при каждом запуске шлюза. Полезно для «при каждом запуске проверять ночные сбои cron и пинговать меня в Discord, если что-то упало» или «суммировать последние 24 деплоя часа.log и публиковать в Slack #ops».

Этот урок покажет, как построить это самостоятельно в виде пользовательского хука. Hermes не требует подключения к хуком BOOT.md — вы настраиваете именно то поведение, которое вам нужно.

Что мы строим

  1. Файл ~/.hermes/BOOT.md, созданный для запуска на естественном языке.
  2. Хук шлюза, который работает на gateway:startup, распределяет блокчейн одноразового агента с разрешённой моделью/учётными данными вашего шлюза и выполняет инструкции из BOOT.md.
  3. Соглашение [SILENT], чтобы агент мог отправлять сообщения, если нечего сообщать.

Шаг 1: Напишите свой чек-лист

создайте ~/.hermes/BOOT.md. Пишите так, как если бы вы дали инструкцию человеку-помощнику:

# Стартовый чек-лист

1. Выполни `hermes cron list` и проверь, не упали ли какие-либо запланированные задачи за ночь.
2. Если какие-то упали, отправь сводку в Discord #ops с помощью инструмента `send_message`.
3. Проверь, есть ли в `/opt/app/deploy.log` строки ERROR за последние 24 часа. Если да, суммируй их и включи в то же сообщение в Discord.
4. Если ничего не пошло не так, ответь только `[SILENT]`, чтобы сообщение не отправлялось.

Агент видит это как часть своего промпта, так что работает всё, что можно описать на естественном языке — вызовы инструментов, шелл-команды, отправка сообщений, суммирование файлов.

Шаг 2: Делаем хук

~/.hermes/hooks/boot-md/
├── HOOK.yaml
└── handler.py

~/.hermes/hooks/boot-md/HOOK.yaml

name: boot-md
description: Выполнять ~/.hermes/BOOT.md при запуске шлюза
events:
  - gateway:startup

~/.hermes/hooks/boot-md/handler.py

"""Выполняет ~/.hermes/BOOT.md при каждом запуске шлюза."""

import logging
import threading
from pathlib import Path

logger = logging.getLogger("hooks.boot-md")

BOOT_FILE = Path.home() / ".hermes" / "BOOT.md"


def _build_prompt(content: str) -> str:
    return (
        "Вы выполняете стартовый чек-лист. Следуйте приведённым ниже инструкциям "
        "точно.\n\n"
        "---\n"
        f"{content}\n"
        "---\n\n"
        "Выполните каждую инструкцию. Используйте инструмент send_message для доставки "
        "любых сообщений на платформы, такие как Discord или Slack.\n"
        "Если ничего не требует внимания и нечего сообщать, ответьте "
        "ТОЛЬКО: [SILENT]"
    )


def _run_boot_agent(content: str) -> None:
    """Порождает одноразового агента и выполняет чек-лист.

    Использует разрешённую модель шлюза и учётные данные времени выполнения,
    так что это работает с пользовательскими конечными точками, агрегаторами
    и провайдерами на основе OAuth.
    """
    try:
        from gateway.run import _resolve_gateway_model, _resolve_runtime_agent_kwargs
        from run_agent import AIAgent

        agent = 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", "")
        if response and "[SILENT]" not in response:
            logger.info("boot-md завершён: %s", response[:200])
        else:
            logger.info("boot-md завершён (нечего сообщать)")
    except Exception as e:
        logger.error("Ошибка агента boot-md: %s", e)


async def handle(event_type: str, context: dict) -> None:
    if not BOOT_FILE.exists():
        return
    content = BOOT_FILE.read_text(encoding="utf-8").strip()
    if not content:
        return

    logger.info("Выполнение BOOT.md (%d символов)", len(content))

    # Фоновый поток, чтобы запуск шлюза не блокировался полным оборотом агента.
    thread = threading.Thread(
        target=_run_boot_agent,
        args=(content,),
        name="boot-md",
        daemon=True,
    )
    thread.start()

Две ключевые строки:

Без них простой AIAgent() будет использовать встроенные значения по умолчанию и получать 401 при подключении к любой нестандартной конечной точке.

Шаг 3: Протестируйте

Перезапустите шлюз:

hermes gateway restart

Следите за логами:

hermes logs --follow --level INFO | grep boot-md

Вы должны увидеть Выполнение BOOT.md (N символов), а затем либо boot-md завершён:... (сводка того, что агент сделал), либо boot-md завершён (нечего сообщать), когда агент ответил [SILENT].

Удалите ~/.hermes/BOOT.md, чтобы отключить чек-лист — хук остается загруженным, но молча пропускает выполнение, когда файла нет.

Расширение узора

Почему это не встроенная функция

Ранняя версия Hermes поставлялась с этим протоколом хуком и молчала агентом блокчейна с базовыми настройками при каждом запуске шлюза. Это удивляло пользователей с пользовательскими конечными точками и делало функцию невидимой для тех, кто не знал, что она работает. Сохранение этого как документированного шаблона — который вы строите сами в вашем каталоге хуков — означает, что вы видите, что именно он делает, и соглашаетесь, записывая файлы.

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

  1. При запуске шлюза HookRegistry.discover_and_load() сканирует ~/.hermes/hooks/
  2. Каждый подкаталог с HOOK.yaml + handler.py загружается.
  3. Обработчики регистрируются для объявленных мероприятий.
  4. В каждой точке рабочего цикла hooks.emit() запускаются все подходящие обработчики.
  5. Ошибки в любом обработчике перехватываются и регистрируются — сломанный хук никогда не возникает у агента:::информация Хуки шлюза срабатывает только в шлюзе (Telegram, Discord, Slack, WhatsApp, Teams). CLI не загружает хуки шлюза. Для хуков, которые работают везде, воспользуйтесь хуки плагинов.

Хуки плагинов (Хуки плагинов)

Плагины могут регистрировать хуки, которые срабатывают в сессиях как CLI, так и шлюза. Они регистрируются программно через ctx.register_hook() в функции register() вашего плагина.

def register(ctx):
    ctx.register_hook("pre_tool_call", my_tool_observer)
    ctx.register_hook("post_tool_call", my_tool_logger)
    ctx.register_hook("pre_llm_call", my_memory_callback)
    ctx.register_hook("post_llm_call", my_sync_callback)
    ctx.register_hook("on_session_start", my_init_callback)
    ctx.register_hook("on_session_end", my_cleanup_callback)

Общие правила для всех хуков:

Краткий справочник

Хук С осуществлением когда Возвращает
pre_tool_call Перед выполнением любого инструмента {"action": "block", "message": str} для внешних вызовов
post_tool_call После возврата любого инструмента теряется
pre_llm_call Один раз за шаг, до начала работы с инструментами {"context": str} для добавления контекста к сообщению пользователя
post_llm_call Один раз за шаг, после завершения цикла, инструменты теряется
on_session_start Создана новая сессия (только первый шаг) теряется
on_session_end Сессия завершается теряется
on_session_finalize CLI/шлюз завершает активную сессию (сброс, сохранение, статистика) теряется
on_session_reset Шлюз меняет ключ сессии (например, /new, /reset) теряется
subagent_stop Дочерний процесс delegate_task завершен теряется
pre_gateway_dispatch Шлюз получил сообщение пользователя, до аутентификации + диспетчеризации {"действие": "пропустить" \| "переписать" \| "разрешить",...} для управления потоком
pre_approval_request Опасная требует подтверждения пользователя, отправки запроса/уведомления теряется
post_approval_response Пользователь ответил на запрос подтверждения (или истёк таймаут) теряется
transform_tool_result После возврата любого инструмента, допередача результата модели str для результата замены, None для оставления без изменений
transform_terminal_output Внутри инструмента терминал, до обрезки/удаления ANSI/редактирования str для замены сырого результата, None для оставления без изменений
transform_llm_output После завершения вызова инструментов, выведите окончательный ответ str для замены текста ответа, None/пусто для оставления без изменений

pre_tool_call

С внедрением непосредственно перед выполнением каждого инструмента — как встроенных, так и инструментов и плагинов.

Подпись обратного вызова:

def my_callback(tool_name: str, args: dict, task_id: str, **kwargs):
Параметр Тип Описание
имя_инструмента ул Имя инструмента, который будет выполнен (например, "terminal", "web_search", "read_file")
аргументы дикт Аргументы, которые модель передала инструменту
task_id ул Идентификатор сессии/задачи. Пустая строка, если не задано.

С реализация: В model_tools.py, внутри handle_function_call(), до выполнения обработки инструмента. Выполняется один раз на каждом вызове инструмента — если модель содержит 3 инструмента одновременно, этот хук выполняется 3 раза.

Возвращаемое значение — отклонение вызова:

return {"action": "block", "message": "Причина блокировки вызова инструмента"}

Агент прерывает выполнение инструмента, возвращая «сообщение» в качестве модели ошибки. Побеждает первую подступающую директиву блокировки (сначала регистрируются плагины Python, затем шелл-хуки). Любое другое возвращаемое значение теряется, поэтому обратные вызовы возникают только для наблюдения, работа продолжается без изменений.

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

Пример — журнал аудита вызовов инструментов:

import json, logging
from datetime import datetime

logger = logging.getLogger(__name__)

def audit_tool_call(tool_name, args, task_id, **kwargs):
    logger.info("ВЫЗОВ_ИНСТРУМЕНТА сессия=%s инструмент=%s аргументы=%s",
                task_id, tool_name, json.dumps(args)[:200])

def register(ctx):
    ctx.register_hook("pre_tool_call", audit_tool_call)

Пример — предупреждение об игровых инструментах:

DANGEROUS = {"terminal", "write_file", "patch"}

def warn_dangerous(tool_name, **kwargs):
    if tool_name in DANGEROUS:
        print(f"⚠ Выполняется потенциально опасный инструмент: {tool_name}")

def register(ctx):
    ctx.register_hook("pre_tool_call", warn_dangerous)

post_tool_call

С осуществлением непосредственно после возврата каждого инструмента.

Подпись обратного вызова:

def my_callback(tool_name: str, args: dict, result: str, task_id: str,
                duration_ms: int, **kwargs):
Параметр Тип Описание
имя_инструмента ул Имя только что законченного инструмента
аргументы дикт Аргументы, которые модель передала инструменту
результат ул Возвращаемое значение инструмента (всегда строки JSON)
task_id ул Идентификатор сессии/задачи. Пустая строка, если не задано.
длительность_мс интервал Сколько времени занял инструмент диспетчеризации, в миллисекундах (измеряется с помощью time.monotonic() вокруг registry.dispatch()).

С реализации: В model_tools.py, внутри handle_function_call(), после возврата обработчика инструмента. Выполняется один раз на каждом вызове инструмента. Не среализован, если инструмент вызвал необработанное выражение (ошибка перехватывается и возвращается как строка JSON с ошибкой, а post_tool_call выполняется с этой строковой ошибкой в ​​качестве result).

Возвращаемое значение: Игнорируется.

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

Пример — отслеживание метрики использования инструментов:

from collections import Counter, defaultdict
import json

_tool_counts = Counter()
_error_counts = Counter()
_latency_ms = defaultdict(list)

def track_metrics(tool_name, result, duration_ms=0, **kwargs):
    _tool_counts[tool_name] += 1
    _latency_ms[tool_name].append(duration_ms)
    try:
        parsed = json.loads(result)
        if "error" in parsed:
            _error_counts[tool_name] += 1
    except (json.JSONDecodeError, TypeError):
        pass

def register(ctx):
    ctx.register_hook("post_tool_call", track_metrics)

pre_llm_call

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

Подпись обратного вызова:

def my_callback(session_id: str, user_message: str, conversation_history: list,
                is_first_turn: bool, model: str, platform: str, **kwargs):
Параметр Тип Описание
session_id ул Уникальный идентификатор текущей сессии
user_message ул Исходное сообщение пользователя для этого шага (обеспечение из навыков)
история_разговора список Копия полного списка сообщений (формат OpenAI: [{"role": "user", "content": "..."}])
is_first_turn бул True, если это первый шаг новой сессии, False для измерения шагов
модель ул Идентификатор модели (например, "anthropic/claude-sonnet-4.6")
платформа ул Где заслуга сессии: "cli", "telegram", "discord" и т.д.

Сбывается: в run_agent.py, внутри run_conversation(), после сжатия контекста, но до базового цикла while. Осуществляется один раз за вызов run_conversation() (т.е. один раз за шаг пользователя), а не один раз за вызов API внутри программных инструментов.

Возвращаемое значение: Если обратный вызов вызывает с ключом "context" или простую непустую фразу, текст добавляется к сообщению словаря пользователя текущего шага. Верните «Нет» для обеспечения ремонта.

# Внедрить контекст
return {"context": "Вспомненные воспоминания:\n- Пользователь любит Python\n- Работает над hermes-agent"}

# Простая строка (эквивалентно)
return "Вспомненные воспоминания:\n- Пользователь любит Python"

# Без внедрения
return None

Куда вводится контекст: Всегда сообщение пользователя, никогда не системный запрос. Это сохраняет кэш-промптов — системное приглашение остается неизменным между шагами, поэтому кэшированные токены используются повторно. Системный запрос — это территория Hermes (инструкции по моделям, необходимое использование инструментов, личность, навыки). Плагины носят контекст вместе с вводом пользователя.

Весь внедрённый контекст является эфемерным — добавляется только во время вызова API. Исходное сообщение пользователя в истории разговора никогда не меняется, и ничего не сохраняется в базе данных сессии.

Когда несколько плагинов возвращают контекст, их выводы объединяются с последовательностями переводов строк в порядке поиска плагинов (по алфавиту имени каталога).

Варианты использования: Извлечение из памяти, внедрение контекста RAG, ограничения по длине, аналитика по шагам.

Пример — извлечение из памяти:

import httpx

MEMORY_API = "https://your-memory-api.example.com"

def recall(session_id, user_message, is_first_turn, **kwargs):
    try:
        resp = httpx.post(f"{MEMORY_API}/recall", json={
            "session_id": session_id,
            "query": user_message,
        }, timeout=3)
        memories = resp.json().get("results", [])
        if not memories:
            return None
        text = "Вспомненный контекст:\n" + "\n".join(f"- {m['text']}" for m in memories)
        return {"context": text}
    except Exception:
        return None

def register(ctx):
    ctx.register_hook("pre_llm_call", recall)

Пример — ограничения по креплению:

POLICY = "Никогда не выполнять команды, удаляющие файлы, без явного подтверждения пользователя."

def guardrails(**kwargs):
    return {"context": POLICY}

def register(ctx):
    ctx.register_hook("pre_llm_call", guardrails)

post_llm_call

Осуществляется один раз за шаг, после завершения вызова инструментов и получения агентом окончательного ответа. Среализовать только при успешных шагах — не реализовать, если шаг был прерван.

Подпись обратного вызова:

def my_callback(session_id: str, user_message: str, assistant_response: str,
                conversation_history: list, model: str, platform: str, **kwargs):
Параметр Тип Описание
session_id ул Уникальный идентификатор текущей сессии
user_message ул Исходное сообщение пользователя для этого шага
ассистент_ответ ул Окончательный текстовый ответ агента на этот шаг
история_разговора список Копия полного списка сообщений после завершения шага
модель ул Идентификатор модели
платформа ул Где эффектная сессия

Сводится: В run_agent.py, внутри run_conversation(), после выхода из инструментария с окончательным ответом. Выполнены условия if Final_response and not Breaked — так что не сработает, когда пользователь прерывает выполнение на середине или агент достигает лимита итераций без ответа.

Возвращаемое значение: Игнорируется.

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

Пример — синхронизация с внешней памятью:

import httpx

MEMORY_API = "https://your-memory-api.example.com"

def sync_memory(session_id, user_message, assistant_response, **kwargs):
    try:
        httpx.post(f"{MEMORY_API}/store", json={
            "session_id": session_id,
            "user": user_message,
            "assistant": assistant_response,
        }, timeout=5)
    except Exception:
        pass  # best-effort

def register(ctx):
    ctx.register_hook("post_llm_call", sync_memory)

Пример — отслеживание длины ответов:

import logging
logger = logging.getLogger(__name__)

def log_response_length(session_id, assistant_response, model, **kwargs):
    logger.info("ОТВЕТ сессия=%s модель=%s символов=%d",
                session_id, model, len(assistant_response or ""))

def register(ctx):
    ctx.register_hook("post_llm_call", log_response_length)

on_session_start

С реализации один раз, когда создаётся совершенно новая сессия. Не сбывает при продолжении сессии (когда пользователь отправляет второе сообщение в параллельной сессии).

Подпись обратного вызова:

def my_callback(session_id: str, model: str, platform: str, **kwargs):
Параметр Тип Описание
session_id ул Уникальный идентификатор новой сессии
модель ул Идентификатор модели
платформа ул Где эффектная сессия

Сбывается: В run_agent.py, внутри run_conversation(), во время первого шага новой сессии — конкретно после построения системного запроса, но до запуска инструментов цикла. Проверка: если не разговор_история (нет предыдущих сообщений = новая сессия).

Возвращаемое значение: Игнорируется.

Варианты использования: Инициализация состояния, ограниченного сеанса, прогрев кэшей, регистрация сеанса во внешнем сервисе, регистрация начала сеанса.

Пример — инициализация кэша сессии:

_session_caches = {}

def init_session(session_id, model, platform, **kwargs):
    _session_caches[session_id] = {
        "model": model,
        "platform": platform,
        "tool_calls": 0,
        "started": __import__("datetime").datetime.now().isoformat(),
    }

def register(ctx):
    ctx.register_hook("on_session_start", init_session)

on_session_end

Выполняется в самом конце каждого вызова run_conversation(), независимо от результата. Также с реализацией обработки вывода CLI, если агент был на середине шага, когда пользователь был отключен.

Подпись обратного вызова:

def my_callback(session_id: str, completed: bool, interrupted: bool,
                model: str, platform: str, **kwargs):
Параметр Тип Описание
session_id ул Уникальный идентификатор сессии
завершен бул True, если агент получил окончательный ответ, False в противном случае
прерванный бул True, если шаг был прерван (пользователь отправил новое сообщение, /stop или отключен)
модель ул Идентификатор модели
платформа ул Где эффектная сессия

Реализация: В двух точках: 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 = {}

def cleanup_session(session_id, completed, interrupted, **kwargs):
    cache = _session_caches.pop(session_id, None)
    if cache:
        # Сброс накопленных данных на диск или во внешний сервис
        status = "завершена" if completed else ("прервана" if interrupted else "сбой")
        print(f"Сессия {session_id} завершена: {status}, {cache['tool_calls']} вызовов инструментов")

def register(ctx):
    ctx.register_hook("on_session_end", cleanup_session)

Пример — отслеживание продолжительности сессии:

import time, logging
logger = logging.getLogger(__name__)

_start_times = {}

def on_start(session_id, **kwargs):
    _start_times[session_id] = time.time()

def on_end(session_id, completed, interrupted, **kwargs):
    start = _start_times.pop(session_id, None)
    if start:
        duration = time.time() - start
        logger.info("ДЛИТЕЛЬНОСТЬ_СЕССИИ сессия=%s секунд=%.1f завершена=%s прервана=%s",
                     session_id, duration, completed, interrupted)

def register(ctx):
    ctx.register_hook("on_session_start", on_start)
    ctx.register_hook("on_session_end", on_end)

on_session_finalize

С внедрением, когда CLI или шлюз завершает активную сессию — например, когда пользователь запускает /new, шлюз собирает виртуальную сессию или CLI завершает работу с активным агентом. Это последний шанс сбросить состояние, судебное решение о завершении сессии, до того, как ваш идентификатор исчезнет.

Подпись обратного вызова:

def my_callback(session_id: str | None, platform: str, **kwargs):
Параметр Тип Описание
session_id str или Нет Идентификатор завершаемой сессии. Может быть None, если активная сессия не была.
платформа ул "cli" или имя платформы обмена сообщениями ("telegram", "discord" и т.д.).

С реализуется: в cli.py (при /new / выводе из CLI) и gateway/run.py (при сбросе или сборке сессии). Всегда парно с on_session_reset на стороне шлюза.

Возвращаемое значение: Игнорируется.

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


on_session_reset

С внедрением, когда шлюз меняет ключ сессии для активного чата — пользователь вызывал /new, /reset, /clear или адаптер выбирал новую сессию после периода бездействия. Это позволяет плагинам реагировать на то, что состояние разговора было очищено, не ожидая следующего on_session_start.

Подпись обратного вызова:

def my_callback(session_id: str, platform: str, **kwargs):
Параметр Тип Описание
session_id ул Идентификатор новой сессии (уже переключён на свежее значение).
платформа ул Название платформы обмена сообщениями.

С реализация: В gateway/run.py, сразу после выделения нового ключа сессии, но до дальнейших обсуждений в предстоящих сообщениях. На шлюзе порядок: on_session_finalize(old_id) → замена → on_session_reset(new_id)on_session_start(new_id) на первом этапе в следующем шаге.

Возвращаемое значение: Игнорируется.

Варианты использования: Сбросить кэши, применить ограничения к session_id, отправить событие «сессия переключена» в аналитику, подготовить новый набор событий.


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


subagent_stop

Осуществление один раз на каждого дочернего агента после завершения delegate_task. Независимо от того, делили ли вы одну задачу или три, этот хук реализуется один раз для каждого дочернего процесса, последовательно в родительском потоке.

Подпись обратного вызова:

def my_callback(parent_session_id: str, child_role: str | None,
                child_summary: str | None, child_status: str,
                duration_ms: int, **kwargs):
Параметр Тип Описание
parent_session_id ул Идентификатор сессии делегирующего родительского агента
child_role ул \| Нет Тег роли оркестратора, установленный для дочернего процесса («Нет», если функция не включена)
child_summary ул \| Нет Окончательный ответ, который дочерний процесс вернул родителю
child_status ул "завершено","не удалось", "прервано" или "ошибка"
длительность_мс интервал Время выполнения дочернего процесса в миллисекундах

Сбывается: В tools/delegate_tool.py, после того как ThreadPoolExecutor.as_completed() обрабатывает все фьючерсы дочерних процессов. Вызов маршалируется в родительском потоке, поэтому авторам хуков не нужно было думать о конкурентном выполнении обратных вызовов.

Возвращаемое значение: Игнорируется.

Варианты использования: Логирование активности оркестрации, накопление длительности дочерних процессов для биллинга, запись записей аудита после делегирования.

Пример — логирование активности оркестратора:

import logging
logger = logging.getLogger(__name__)

def log_subagent(parent_session_id, child_role, child_status, duration_ms, **kwargs):
    logger.info(
        "ПОДАГЕНТ родитель=%s роль=%s статус=%s длительность_мс=%d",
        parent_session_id, child_role, child_status, duration_ms,
    )

def register(ctx):
    ctx.register_hook("subagent_stop", log_subagent)
```:::информация
При интенсивном делегировании (например, ролик оркестратора × 5 листьев × вложенная глубина) `subagent_stop` выполняется много раз за шаг. Держите обратный вызов быстро; переносите затратные операции в фоновую очередь.</div>
---

### `pre_gateway_dispatch`

Осуществляется **один раз на каждое входящее `MessageEvent`** в шлюзе, после проверки событий обнаружения, но **до** аутентификации/связывания и диспетчеризации агента. Это точка перехвата для политического потока сообщений на уровне шлюза (режимы только для чтения, передачи человеку, маршрутизации по чатам и т. д.), что не происходит ни в одном адаптере платформы.

**Подпись обратного вызова:**
```python
def my_callback(event, gateway, session_store, **kwargs):
Параметр Тип Описание
событие СобытиеСообщения Нормализованное входящее сообщение (имеет .text, .source, .message_id, .internal и т.д.).
шлюз GatewayRunner Активный шлюз-раннер, чтобы подключить различные варианты gateway.adapters[platform].send(...) для ответов по побочным каналам (уведомления владельцу и т.д.).
session_store SessionStore Для бесшумного приема стенограммы через session_store.append_to_transcript(...).

Сбывается: В gateway/run.py, внутри GatewayRunner._handle_message(), сразу после вычисления is_internal. Внутренние события полностью пропускают этот хук (они генерируют систему — обеспечивают фоновые процессы и т.д. — и не должны контролироваться пользовательской политикой).

Возвращаемое значение: None или словарь. Первый распознанный словарь действует побеждает; остальные результаты плагинов отключаются. Исключения в обратных вызовах плагинов перехватываются и регистрируются; В случае ошибки шлюз всегда обращается к нормальной диспетчеризации.

Возврат Эффект
{"действие": "пропустить", "причина": "..."} Отбросить сообщение — никакого ответа агента, никаких ссылок, никаких аутентификаций. Предполагается, что штекер уже обработал его (например, бесшумно принял в стенограмму).
{"action": "rewrite", "text": "новый текст"} Замените event.text, затем продолжите нормальную диспетчеризацию с изменённым событием. Полезно для поворота буферизованных фоновых сообщений в одном запросе.
{"действие": "разрешить"} / Нет Нормальная диспетчеризация — полная цепочка аутентификации/связывания/цикла агента.

Варианты использования: Групповые чаты только для чтения (отвечать только при упоминании; буферизовать фоновые сообщения в контексте); передача человеку (бесшумно принимается сообщение клиенту, пока владелец обрабатывает чат вручную); ограничение скорости для каждого профиля; маршрутизация на основе политики.

Пример — отклонение несанкционированных личных сообщений без активации кода ссылки:

def deny_unauthorized_dms(event, **kwargs):
    src = event.source
    if src.chat_type == "dm" and not _is_approved_user(src.user_id):
        return {"action": "skip", "reason": "unauthorized-dm"}
    return None

def register(ctx):
    ctx.register_hook("pre_gateway_dispatch", deny_unauthorized_dms)

Пример — перезапись буфера фоновых сообщений в один запрос при упоминании:

_buffers = {}

def buffer_or_rewrite(event, **kwargs):
    key = (event.source.platform, event.source.chat_id)
    buf = _buffers.setdefault(key, [])
    if _bot_mentioned(event.text):
        combined = "\n".join(buf + [event.text])
        buf.clear()
        return {"action": "rewrite", "text": combined}
    buf.append(event.text)
    return {"action": "skip", "reason": "ambient-buffered"}

def register(ctx):
    ctx.register_hook("pre_gateway_dispatch", buffer_or_rewrite)

pre_approval_request

С выполнением непосредственно перед показом запроса на подтверждение запроса — соответствующие все поверхности: интерактивный CLI, платформа TUI Ink, шлюз (Telegram, Discord, Slack, WhatsApp, Matrix и т.д.) и клиенты ACP (VS Code, Zed, JetBrains).

Это подходящее место для подключения пользовательского нотификатора — например, приложение в строке меню macOS, показывает с кнопками разрешить/запретить, или журнал аудита, записывающего каждый запрос подтверждения с контекстом.

Подпись обратного вызова:

def my_callback(
    command: str,
    description: str,
    pattern_key: str,
    pattern_keys: list[str],
    session_key: str,
    surface: str,
    **kwargs,
):
Параметр Тип Описание
команда ул Shell-команда, ожидающая подтверждения
описание ул Понятное человеку объяснение причины(ин), по которому команда помечена (объединяется, когда происходит несколько шаблонов)
шаблон_ключ ул Первичный ключ-шаблон, вызвавший запрос (например, "rm_rf", "sudo")
pattern_keys список[стр] Все ключи с узорами, которые совпали
session_key ул Идентификатор сессии, используемый для ограничений доступа к чатам
поверхность ул "cli" для интерактивных подсказок CLI/TUI, "gateway" для асинхронных подтверждений на платформах

Возвращаемое значение: теряется. Хуки здесь только для наблюдения; они не могут отклонить или предварительно ответить на подтверждение. Используйте pre_tool_call, чтобы заблокировать инструмент до того, как он достигнет системы подтверждения.

Варианты использования: Уведомления на рабочем столе, push-уведомления, аудит-логи, вебхуки Slack, маршрутизация эскалации, метрики.

Пример —. на рабочем столе macOS:

import subprocess

def notify_approval(command, description, session_key, **kwargs):
    title = "Hermes требует подтверждения"
    body = f"{description}: {command[:80]}"
    subprocess.Popen([
        "osascript", "-e",
        f'display notification "{body}" with title "{title}"',
    ])

def register(ctx):
    ctx.register_hook("pre_approval_request", notify_approval)

post_approval_response

С выполнением после того, как пользователь ответил на запрос подтверждения (или истёк таймаут).

Подпись обратного вызова:

def my_callback(
    command: str,
    description: str,
    pattern_key: str,
    pattern_keys: list[str],
    session_key: str,
    surface: str,
    choice: str,
    **kwargs,
):

Те же самые kwargs, что и у pre_approval_request, плюс:

Параметр Тип Описание
выбор ул Одно из «однажды», «сеанса», «всегда», «запретить» или «тайм-аут»

Возвращаемое значение: теряется.

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

def log_decision(command, choice, session_key, **kwargs):
    logger.info("подтверждение %s: %s для сессии %s", choice, command[:60], session_key)

def register(ctx):
    ctx.register_hook("post_approval_response", log_decision)

transform_tool_result

Осуществление после возврата инструмента и до добавления результата в разговор. Посмотрите плагин, перепишите результат ЛЮБОГО инструмента — не только выводите терминал — до того, как модель его увидит.

Подпись обратного вызова:

def my_callback(
    tool_name: str,
    arguments: dict,
    result: str,
    task_id: str | None,
    **kwargs,
) -> str | None:
Параметр Тип Описание
имя_инструмента ул Инструмент, который создал результат (read_file, web_extract, delegate_task,...).
аргументы дикт Аргументы, с помощью которых возникла модель данного инструмента.
результат ул Сырой результат инструмента, после обрезки и удаления ANSI.
task_id ул \| Нет Идентификатор задачи/сессии при выполнении в среде RL/бенчмарков.

Возвращаемое значение: str для замены результата (возвращенная строка будет видна модели), None для оставления без изменений.

Варианты использования: Редактировать PII организации из вывода web_extract, обернуть длинные JSON-ответы инструментов в заголовке сводки, внедрить подсказки включения с дополнениями в результаты read_file, переписать отчёты подчинённых delegate_task в схему, специфичную для проекта.

import re
SECRET = re.compile(r"sk-[A-Za-z0-9]{32,}")

def redact_secrets(tool_name, result, **kwargs):
    if SECRET.search(result):
        return SECRET.sub("[РЕДАКТИРОВАНО]", result)
    return None

def register(ctx):
    ctx.register_hook("transform_tool_result", redact_secrets)

Применяется к каждому инструменту. Для перезаписи только терминала см. transform_terminal_output ниже — он более узкий и прогрессирует раньше на конвейере (до обрезки, до редактирования).


transform_terminal_output

Сработка внутри конвейера вывода инструмента терминал до обработка обрезки 50 КБ, удаление ANSI и доработка секретов. Используйте плагины, чтобы переписать сырой stdout/stderr команды оболочки до того, как с ним начнёт работать следующая обработка.

Подпись обратного вызова:

def my_callback(
    command: str,
    output: str,
    exit_code: int,
    cwd: str,
    task_id: str | None,
    **kwargs,
) -> str | None:
Параметр Тип Описание
команда ул Shell-команда, создавшая вывод.
выход ул Сырой объединённый stdout/stderr (может быть очень большим — обрезка происходит после хука).
код_выхода интервал Код процесса возврата.
cwd ул Рабочая директория, в которой выполнялась команда.

Возвращаемое значение: str для замены результатов, None для оставления без изменений.

Варианты использования: Внедрить сводки для команды, которые создают огромные выводы (du -ah, find, tree), пометить вывод маркером, специфичным для проекта, чтобы нижестоящие хуки знали, как с этим обращаться, удалять шум времени выполнения, которое меняется между запусками и кэшированием промптов.

def summarize_find(command, output, **kwargs):
    if command.startswith("find ") and len(output) > 50_000:
        lines = output.count("\n")
        head = "\n".join(output.splitlines()[:40])
        return f"{head}\n\n[сводка: всего {lines} путей, показаны первые 40]"
    return None

def register(ctx):
    ctx.register_hook("transform_terminal_output", summarize_find)

Хорошо сочетается с «transform_tool_result» (который соответствует всем остальным инструментам).


transform_llm_output

Осуществляется один раз за шаг после выполнения инструментов вызова цикла и получения модели окончательного ответа, до того, как этот ответ будет доставлен пользователю (CLI, шлюз или программный вызывающий). Рассмотрим плагин, переписывающий окончательный текст ассистента с помощью классических программных методов — без дополнительных токенов вывода, потраченных на текст SOUL или преобразование, управляемых навыками.

Подпись обратного вызова:

def my_callback(
    response_text: str,
    session_id: str,
    model: str,
    platform: str,
    **kwargs,
) -> str | None:
Параметр Тип Описание
текст_ответа ул Заключительный текст ответа ассистента для этого шага.
session_id ул Идентификатор сессии для этого разговора (может быть пустым для одноразовых запусков).
модель ул Имя модели, создавшей ответ (например, anthropic/claude-sonnet-4.6).
платформа ул Платформа доставки (cli, telegram, discord, …; пусто, если не задано).

Возвращаемое значение: Непустая str для замены текста ответа, None или пустая строка для оставления без изменений. Первая непустая строка побеждает, когда зарегистрировано несколько плагинов — обозначается transform_tool_result.

Варианты использования: Применить преобразование личности/словаря (пиратская речь, Губка Боб), администраторы идентификаторов, специальные для пользователя, из конечного текста, снизу добавить колонтитул, специальный для проекта, обеспечить соблюдение правил по стилю без затрат токенов на инструкции SOUL.

import os, re

def spongebob(response_text, **kwargs):
    if os.environ.get("SPONGEBOB_MODE")!= "on":
        return None  # пропустить без изменений
    return re.sub(r"!", "!! Тартарный соус!", response_text)

def register(ctx):
    ctx.register_hook("transform_llm_output", spongebob)

Хук защищён от непустого, не прерванного ответа — он не срабатывает при нажатии кнопки остановки или пустых шагах. Исключения регистрируются как обращение и не прерывают выполнение агента.


Шелл-хуки (Крючки-ракушки)

Объявите шелл-скриптовые хуки в файле cli-config.yaml, и Hermes будет запускать их как подпроцессы в любой момент, когда реализация включает плагин события хука — как в CLI, так и в сессиях шлюза. Не требуется создание плагинов на Python.

Используйте шелл-хуки, когда вам нужен подключаемый однофайловый скрипт (Bash, Python, что угодно с shebang), чтобы:

Шелл-хуки регистрирует вызов agent.shell_hooks.register_from_config(cfg) при запуске как CLI (hermes_cli/main.py), так и шлюза (gateway/run.py). Они естественно сочетаются с хуками плагинов Python — оба передаются через одного и того же диспетчера.

Сравнение с взглядом первого

Измерение Шелл-хуки Хуки плагинов Хуки шлюза
Объявляются в заблокировать hooks: в ~/.hermes/config.yaml register() в плагинеplugin.yaml каталог HOOK.yaml + handler.py
Находятся в ~/.hermes/agent-hooks/ (по соглашению) ~/.hermes/plugins/<имя>/ ~/.hermes/hooks/<имя>/
Язык Любой (Bash, Python, Go-бинарник, …) Только Питон Только Питон
Выполняется в CLI + шлюз CLI + шлюз Только шлюз
События VALID_HOOKS (включая subagent_stop) VALID_HOOKS Жизненный цикл шлюза (шлюз:запуск, агент:*, команда:*)
Можно заблокировать инструмент вызова Да (pre_tool_call) Да (pre_tool_call) Нет
Может внедрить контекст LLM Да (pre_llm_call) Да (pre_llm_call) Нет
Согласие Первый запрос на каждую пару (событие, команда) Неявно (доверие к плагину Python) Неявно (доверие к каталогу)
Межпроцессная изоляция Да (подпроцесс) Нет (внутри процесса) Нет (внутри процесса)

Схема конфигурации

hooks:
  <event_name>:                  # Должно быть в VALID_HOOKS
    - matcher: "<regex>"         # Опционально; используется только для pre/post_tool_call
      command: "<команда shell>" # Обязательно; выполняется через shlex.split, shell=False
      timeout: <секунды>         # Опционально; по умолчанию 60, не более 300

hooks_auto_accept: false         # См. "Модель согласия" ниже

Имена событий должны быть одним из событий хуков плагинов; опечатки вызывают предупреждение «Возможно, вы имели в виду X?» и про результат. Неизвестные ключи внутри одной записи игнорируются; отсутствие команды приводит к пропуску с предупреждением. timeout > 300 обрезается с предупреждением.

Протокол JSON через канал

Каждый раз, когда событие происходит, Hermes генерирует подпроцесс для каждого подходящего хука (с учётом фильтра), передаёт JSON-нагрузку на stdin и считывает stdout обратно как JSON.

stdin — нагрузка, получает какой скрипт:

{
  "hook_event_name": "pre_tool_call",
  "tool_name":       "terminal",
  "tool_input":      {"command": "rm -rf /"},
  "session_id":      "sess_abc123",
  "cwd":             "/home/user/project",
  "extra":           {"task_id": "...", "tool_call_id": "..."}
}

tool_name и tool_input равны null для событий, не связанных с инструментами (pre_llm_call, subagent_stop, жизненный циклический сеанс). Словарь extra содержит все специфичные для событий kwargs (user_message, conversation_history, child_role, duration_ms,...). Несериализуемые значения преобразуются в строки, а не в результате.

stdout — опциональный ответ:

// Блокировка pre_tool_call (принимаются обе формы; нормализуются внутри):
{"decision": "block", "reason":  "Запрещено: rm -rf"}   // Стиль Claude-Code
{"action":   "block", "message": "Запрещено: rm -rf"}   // Канонический Hermes

// Внедрение контекста для pre_llm_call:
{"context": "Сегодня пятница, 2026-04-17"}

// Бездействие  любой пустой / несовпадающий вывод подходит:

Недействительный JSON, ненулевые коды возврата и таймауты регистрируют предупреждение, но цикл агента никогда не прерывается.

Примеры работы

1. Автоформатирование Python-файлов после каждой записи

# ~/.hermes/config.yaml
hooks:
  post_tool_call:
    - matcher: "write_file|patch"
      command: "~/.hermes/agent-hooks/auto-format.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/auto-format.sh
payload="$(cat -)"
path=$(echo "$payload" | jq -r '.tool_input.path // empty')
[[ "$path" == *.py ]] && command -v black >/dev/null && black "$path" 2>/dev/null
printf '{}\n'

Представление файла в девятом агенте не пересчитывается автоматически — переформатирование влияет только на файл на диске. Последующие вызовы read_file создают отформатированную версию.

2. Блокировка деструктивных команд терминал

hooks:
  pre_tool_call:
    - matcher: "terminal"
      command: "~/.hermes/agent-hooks/block-rm-rf.sh"
      timeout: 5
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/block-rm-rf.sh
payload="$(cat -)"
cmd=$(echo "$payload" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -qE 'rm[[:space:]]+-rf?[[:space:]]+/'; then
  printf '{"decision": "block", "reason": "заблокировано: rm -rf / не разрешена"}\n'
else
  printf '{}\n'
fi

3. Внедрение git status на каждом шаге (эквивалент Claude-Code UserPromptSubmit)

hooks:
  pre_llm_call:
    - command: "~/.hermes/agent-hooks/inject-cwd-context.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/inject-cwd-context.sh
cat - >/dev/null   # отбросить нагрузку stdin
if status=$(git status --porcelain 2>/dev/null) && [[ -n "$status" ]]; then
  jq --null-input --arg s "$status" \
     '{context: ("Незакоммиченные изменения в cwd:\n" + $s)}'
else
  printf '{}\n'
fi

Событие UserPromptSubmit от Claude Code намеренно не является событием Hermes — pre_llm_call выполняется в том же месте и уже поддерживает реализацию контекста. Используйте его здесь.

4. Логирование каждого содержания подчинённого

hooks:
  subagent_stop:
    - command: "~/.hermes/agent-hooks/log-orchestration.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/log-orchestration.sh
log=~/.hermes/logs/orchestration.log
jq -c '{ts: now, parent:.session_id, extra:.extra}' < /dev/stdin >> "$log"
printf '{}\n'

Модель соглашения

Каждый элемент пара (event, команда) запрашивает у пользователя подтверждение, когда Hermes впервые его видит, а затем сохраняет решение в ~/.hermes/shell-hooks-allowlist.json. При последующем запуске (CLI или шлюз) запрос пропускают.

Три выхода обойти интерактивный запрос — достаточно любого:

  1. Флаг --accept-hooks в CLI (например, hermes --accept-hookschat)
  2. Переменная окружения HERMES_ACCEPT_HOOKS=1
  3. hooks_auto_accept: true в cli-config.yaml

Запуски не в TTY (шлюз, cron, CI) требуют одного из трех вариантов — в противном случае любой недавно добавленный хук остается незарегистрированным и регистрирует предупреждение.

Редактирование скриптов молча доверяется. Список разрешений привязывается к точной строке команды, а не к хэшу скрипта, поэтому редактирование скрипта на диске не отменяет согласия. Гермес Крючки Доктор фиксирует перемещение времени, чтобы вы могли увидеть правки и решить, нужно ли повторно восстановить.

CLI гермесовые хуки

Команда Что делает
список крючков Гермеса Вывести настроенные хуки с фильтром, таймаутом и статусом соглашения
тест перехватчиков Гермеса <событие> [--for-tool X] [--payload-file F] Запустить все подсоединения хуки с синтетической конфигурацией и вывести распарсенный ответ
хуки Гермеса отозвать <команду> Удалить все записи из списка разрешений, соответствует <command> (вступает в силу при следующем перезапуске)
гермес крючки доктор Для каждого настроенного хука: проверьте исполняемость, статус в списке разрешений, расхождение mtime, валидность JSON-вывода и примерное битовое время выполнения

Безопасность

Шелл-хуки выполняются с вашими полными учётными данными пользователя — та же граница безопасности, что и запись cron или псевдоним оболочки. Относитесь к блоку hooks: в config.yaml как к привилегированной конфигурации:

Порядок и приоритет

Как хуки плагинов Python, так и шелл-хуки передаются через один и тот же диспетчер invoke_hook(). Плагины Python регистрируются первыми (discover_and_load()), шелл-хуки первыми (register_from_config()), поэтому в спорных случаях решения по блокировке с помощью Python pre_tool_call имеют приоритет. Первая валидная блокировка побеждает — агрегатор возвращает результат, как только любой обратный вызов вызывает {"action": "block", "message": str} с непустым сообщением.