Содержание
  1. Ветка 2.x и запрет на перенос V1
  2. Серверная поверхность плагина
  3. Локальный call-audit.ts
  4. Поля журнала и приватность
  5. Корреляция по sessionID и id
  6. Состояния: загрузка, before, completed, error
  7. Таблица диагностики
  8. Упражнение на учебном проекте
  9. Границы наблюдения
  10. Частые вопросы
Гайды

Как настроить хуки OpenCode V2 и проверить вызовы

5 ноября 2026 · 10 минут чтения

Как настроить хуки OpenCode V2 и проверить вызовы. Хуки OpenCode V2 настраивают в локальном серверном плагине. В ветке 2.x хук execute.after получает один изменяемый объект события; в V1 использовались отдельные объекты input и output. Реализацию V1 нужно переписать для V2. Ниже — учебный плагин, ключ корреляции и таблица диагностики от загрузки до фактического пути вызова.

Ветка 2.x и запрет на перенос V1

Документация OpenCode по миграции прямо предупреждает: реализации плагинов V1 в V2 не запускаются, перемещения файла или переименования записи в конфиге недостаточно. В V1 плагин возвращал один объект с инструментами и хуками, где ключом служила строка вроде tool.execute.after. В V2 плагин экспортирует по умолчанию определение с id и setup(ctx), а хуки регистрируются через контекст домена. Поэтому переносить возвращаемый объект tool.execute.after в V2 бессмысленно: там нет механизма, который его прочитает.

Таблица миграции из документации сопоставляет старые ключи с новыми API. Для инструментов указано: tool.execute.before соответствует ctx.tool.hook("execute.before", ...), а tool.execute.after — ctx.tool.hook("execute.after", ...). Это адреса переноса. Код нужно переписать на регистрацию в setup, иначе хук останется в старом файле и не сработает.

Конфигурацию документация предлагает нормализовать автоматически: ключ plugin заменяется на plugins, а кортеж «пакет и опции» — на объект с полями package и options. Локальные плагины V2 размещают в .opencode/plugins/. Настроенный локальный путь обязан указывать на каталог: файл ./plugin/local.ts пропускается с предупреждением в логе, а реализацию переносят в ./plugin/local/index.ts.

Серверная поверхность плагина

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

setup выполняется при загрузке плагина и может вернуть функцию очистки, которая срабатывает при выгрузке. Внутри setup доступны ctx.location.directory и ctx.location.project.canonical — расположение экземпляра плагина. Документация отдельно оговаривает: это расположение самого плагина, к которой он имеет доступ, и не каждого события, которое получает. Для журнала вызовов это значит, что путь загрузки и путь конкретного вызова — разные вещи, и их стоит различать в диагностике.

Хуки перехватывают живые операции. Хук регистрируется на домене, который владеет операцией, а возвращённая регистрация освобождается, когда перестаёт быть нужна. Несколько плагинов могут зарегистрировать один и тот же хук; OpenCode запускает их в порядке плагинов, поэтому поздние хуки видят изменения ранних.

Локальный call-audit.ts

Ниже — один локальный плагин .opencode/plugins/call-audit.ts с экспортом по умолчанию Plugin.define и стабильным id. Идентификатор нужен каждому плагину V2: хранилище плагина привязано к этому ID, и он же опознаёт плагин в статусе и диагностике.

TS40 строк
import { Plugin } from "@opencode/plugin"

type AuditRecord = {
  time: string
  phase: "before" | "after"
  sessionID: string
  id: string
  tool: string
  status?: "completed" | "error"
}

export default Plugin.define({
  id: "local.call-audit",
  async setup(ctx) {
    const write = (record: AuditRecord) => {
      console.log(JSON.stringify(record))
    }

    await ctx.tool.hook("execute.before", (event) => {
      write({
        time: new Date().toISOString(),
        phase: "before",
        sessionID: event.sessionID,
        id: event.id,
        tool: event.tool,
      })
    })

    await ctx.tool.hook("execute.after", (event) => {
      write({
        time: new Date().toISOString(),
        phase: "after",
        sessionID: event.sessionID,
        id: event.id,
        tool: event.tool,
        status: event.status,
      })
    })
  },
})

Поле id в событиях — это Tool.CallID из типов плагина, то есть идентификатор конкретного вызова инструмента. В объяснении его удобно называть callID, но в коде берётся именно event.id. Оба хука регистрируются через await, поэтому регистрация завершается до того, как плагин начнёт наблюдать вызовы.

Поля журнала и приватность

Учебный журнал хранит шесть полей: time, phase, sessionID, id, tool, status. Поля input, result и содержимое сообщений в него не попадают. В типах execute.before и execute.after присутствует поле input, а у завершённого вызова — result, и именно их учебный пример намеренно не пишет. Аргументы инструмента и результаты могут содержать пути, фрагменты файлов и другие данные, которые в диагностический журнал попадать не должны.

status появляется только в фазе after. В типах execute.after это объединение: либо status: "completed" с полем result, либо status: "error" с полем error. Запись фазы before поля status не имеет, и это нормально: до выполнения инструмента итог ещё неизвестен.

Поле before after Назначение
time есть есть Момент записи
phase before after Этап хука
sessionID есть есть Сессия вызова
id есть есть Идентификатор вызова
tool есть есть Имя инструмента
status нет completed или error Итог выполнения

Корреляция по sessionID и id

Записи сопоставляются по паре sessionID и id. Имя инструмента tool в ключ корреляции не входит: два вызова read в одной сессии — это два разных вызова с разными id. Если сопоставлять по имени инструмента, журнал склеит разные операции и покажет ложную картину.

Учебный пример корреляции из принятого артефакта выглядит так: запись before с sessionID: "demo-session", id: "demo-call-7", tool: "read" и запись after с теми же sessionID и id, тем же tool и status: "completed". Это условный вход для разбора. Пара sessionID и id совпала — значит, обе записи относятся к одному вызову.

sessionID id tool before after.status
demo-session demo-call-7 read есть completed
demo-session demo-call-8 read есть error
demo-session demo-call-9 read есть нет записи

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

Состояния: загрузка, before, completed, error

Диагностика разводит пять состояний. Загрузка плагина — есть ли id и источник в списке активных плагинов. before — зарегистрировался ли хук и дошёл ли вызов до него. completed — запись after со статусом завершения. error — запись after со статусом ошибки. Отсутствие парного after — отдельное состояние, которое не приравнивается к ошибке инструмента.

Порядок проверки идёт от общего к частному. Сначала подтверждают, что плагин загружен и его id виден в списке активных. Затем проверяют, что хук execute.before зарегистрирован и записывает вызовы. После этого смотрят, появляется ли парная запись after и какой у неё status. Пропуск after разбирают отдельно, сопоставляя его с моментом выгрузки плагина и с тем, дошёл ли вызов до серверного хука.

Документация по миграции предлагает проверять перенесённый плагин по шагам: подтвердить, что его ID и источник видны в списке активных плагинов; прогнать каждый зарегистрированный хук, трансформацию, инструмент, команду и подписку на события; проверить очистку перезагрузкой или удалением плагина; протестировать опции и сохранённое состояние на чистом проекте. Для опубликованного пакета советуют тестировать установленную версию.

Таблица диагностики

Таблица ведёт от версии и источника загрузки к регистрации и фактическому пути вызова. Каждая строка — отдельная проверка со своим признаком и выводом.

Шаг Что проверяем Признак Вывод
1 Версия ветки 2.x V1-код не переносится
2 Источник загрузки .opencode/plugins/ или запись в plugins Плагин найден
3 ID в списке активных local.call-audit Плагин загружен
4 Регистрация хуков await в setup Хуки активны
5 Путь вызова запись before Вызов дошёл до хука
6 Итог вызова after.status completed или error
7 Парная запись after отсутствует Отдельное состояние

Строка 2 опирается на правила загрузки: плагины в .opencode/plugins/ загружаются автоматически, а опубликованные пакеты и каталоги из других мест добавляют в plugins в opencode.json(c). Одна запись загружает все части плагина. Строка 3 использует список активных плагинов, доступный через контекст. Строки 5 и 6 опираются на записи журнала, а строка 7 фиксирует пропуск как самостоятельный случай.

Упражнение на учебном проекте

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

Порядок действий:

  1. Создайте отдельный каталог проекта и файл .opencode/plugins/call-audit.ts с кодом выше.
  2. Откройте проект в OpenCode и убедитесь, что local.call-audit виден в списке активных плагинов.
  3. Дайте агенту задачу прочитать обычный текстовый файл внутри проекта.
  4. Найдите в выводе две записи с одинаковыми sessionID и id.
  5. Сверьте phase и status второй записи.
  6. Сравните две записи по таблице корреляции.

Ожидаемый результат — пара записей, где before идёт без status, а after несёт status: "completed". Если парной записи нет, зафиксируйте это как пропуск и проверьте шаги 2–4 таблицы диагностики. Если запись after несёт status: "error", посмотрите на сам факт ошибки, не выводя содержимое error в журнал.

Границы наблюдения

Хук execute.before и execute.after наблюдает вызовы инструментов, прошедшие через этот хук в серверном процессе. Приложение, его политика безопасности и правила разрешений — другие уровни, и журнал вызовов их не заменяет. Запись в журнале подтверждает факт прохождения вызова через хук.

Правила разрешений в OpenCode устроены отдельно: ctx.permission.assert оценивает правила сессии, включая сохранённые одобрения, и запрашивает пользователя, когда правило сводится к вопросу. Хук evaluate проверяет решения о разрешениях после оценки настроенных правил и до выполнения действия или публикации запроса. Явный запрет в конфигурации окончателен и хук не вызывает. Эти механизмы живут на своём уровне, и учебный журнал их не дублирует.

Событие session.idle относится к подписке на поток событий сервера. Строить вывод о завершении вызова на нём не стоит: для этого есть парная запись after с её status. Подписка на события — отдельная возможность контекста, и её поведение описывается в документации по событиям.

Частые вопросы

Как связать записи before и after одного вызова?
Ключ корреляции — пара sessionID и id. Оба поля присутствуют в событиях execute.before и execute.after. Имя инструмента в ключ не входит, потому что два вызова одного инструмента имеют разные id.

Почему хук execute.after не срабатывает?
Возможные причины стоит проверять по порядку: плагин не загружен, хук не зарегистрирован в setup, вызов не дошёл до серверного хука. Пропуск парной записи сам по себе не доказывает ошибку инструмента. Начните с шагов 2–5 таблицы диагностики.

Можно ли перенести код V1 без изменений?
Нет. Реализации V1 в V2 не запускаются, а возвращаемый объект с ключом tool.execute.after в V2 не читается. Хуки регистрируют через ctx.tool.hook в setup, а конфигурацию переводят с plugin на plugins.

Какие поля безопасно писать в журнал?
time, phase, sessionID, id, tool и status. Поля input, result и содержимое сообщений в учебный журнал не пишут: они могут нести данные проекта. status появляется только в фазе after.

Заменяет ли журнал правила безопасности?
Нет. Журнал фиксирует вызовы, прошедшие через хук инструментов. Правила разрешений и политика приложения работают на своих уровнях, и явный запрет в конфигурации хук evaluate не вызывает.

Читайте также

3 материала