.agents/skills/opencode-qa/references/events-hooks.md
opencode publishes lifecycle events over Server-Sent Events. Plugins observe the SAME events via the event hook, so confirming an event on the wire proves a hook would fire. The bundled probe is scripts/sse-hook-probe.sh.
server.connected, a server.heartbeat arrives every 10s, and the stream ends on server.instance.disposed.data: {"type":"...","properties":{...}} (one per line). Consume with curl -N.curl -N -u opencode:$PASS "http://127.0.0.1:4096/event?directory=$PWD"
Bundled, with assertions + auto-teardown:
scripts/sse-hook-probe.sh --self-test
(spawns an isolated server, asserts server.connected)
scripts/sse-hook-probe.sh --attach http://127.0.0.1:4096 --password "$PASS" --directory "$PWD" --event message.part.updated --timeout 30
(watch your real server for a specific event)
session.created / session.updated / session.deleted (sessionID, info)message.updated (sessionID, info)message.removed (sessionID, messageID)message.part.updated (sessionID, part, time)message.part.delta (sessionID, messageID, partID, field, delta)message.part.removedpermission.asked (id, sessionID, permission, tool?)permission.repliedsession.error (sessionID?, error)session.diff (sessionID, diff)question.asked / question.replied / question.rejectedfile.watcher.updated (file, event)project.updatedlsp.updatedpty.created / pty.updated / pty.exited / pty.deletedserver.connectedserver.heartbeatserver.instance.disposedglobal.disposedplugin.addedTwo-shell pattern (or use the script):
# shell 1: watch (kill with Ctrl-C when done)
curl -N -u opencode:$PASS "http://127.0.0.1:4096/event?directory=$PWD" \
| grep --line-buffered '"type":"message.part.updated"'
# shell 2: trigger an action (fire-and-forget)
curl -X POST -u opencode:$PASS -H 'Content-Type: application/json' \
-d '{"parts":[{"type":"text","text":"say hi"}]}' \
"http://127.0.0.1:4096/session/<ses_id>/prompt_async?directory=$PWD"
A message.part.updated (text/tool) confirms the prompt action drove the model and any tool/permission hook path. Note: a real prompt requires a configured provider/auth, so this runs against your real server, not the isolated sandbox (the sandbox only proves the SSE plumbing via server.connected).
event, config, tool, auth, provider, chat.message, chat.params, chat.headers, permission.ask, command.execute.before, tool.execute.before, tool.execute.after, tool.definition, shell.env, experimental.chat.messages.transform, experimental.chat.system.transform, experimental.session.compacting, experimental.compaction.autocontinue, experimental.text.complete.
{ id?, server: (input, options) => Promise<Hooks> }.event and tool.execute.before that console.log the activity:export default {
id: "qa-logger",
async server(input, options) {
return {
event: async (event) => {
console.log("[event]", event.type, event.properties);
},
"tool.execute.before": async (tool, args, context) => {
console.log("[tool.before]", tool.name, args);
},
};
},
};
plugin / plugin_origins array (project .opencode/ config or user config), then restart opencode. On load it emits plugin.added.Pair this with references/server-api.md (how to start the server, auth, prompt routes).