What I found
OpenCode subagents share their parent's plugin hooks. The tool.execute.after hook fires for every subagent session. It receives the session ID and the tool output. And it can mutate that output before the model sees it.
This means you can splice a correction into a running subagent's context mid-turn. No daemon, no polling, no bash tool that the model might skip. The hook fires deterministically.
How it works
The operator writes a message to a maildir spool (new/ directory). The plugin hook, running on every tool.execute.after, checks the spool, atomically moves the message to cur/, and appends it to the tool output text. The language model sees it on its next reasoning step:
⚠️ [SESSION-BUS] Messages:
[cto→qa-agent]!! [correction]: Use bcrypt not sha256 for password hashing
The maildir gives crash-safe delivery without any locks. A message being written stays in tmp/. Once fully written and synced, an atomic rename puts it in new/. The reader atomically renames it to cur/ when delivered. Kill the process at any point and you either get the message or you do not. Partial reads are impossible.
The code
Working implementation at https://github.com/semanticRig/cross-session-agent-messaging
About 500 lines total: a Python core library for the spool, a TypeScript plugin for the injection hooks, and MCP server wrappers for the tools. Tested on OpenCode 1.18.15 with 10 parallel subagents.
Honest limitations
- The hook fires after a tool call. If the subagent emits a final answer with no further tool call, the message lands on the next tool turn, not retroactively.
- Delivery is at-least-once. A crash between the rename into
new/ and the move to cur/ can double-deliver. The message ID handles deduplication.
- This uses an undocumented hook contract. The payload shape can change between OpenCode versions.
Why this matters
Claude Code added agent teams in 2.1.224. You can correct a subagent mid-task from another terminal. OpenCode has no equivalent. But the plugin system makes it possible to build one with remarkably little code.
I wrote up the technical details and the discovery process at the repo linked above. Would love feedback, especially from anyone who has worked with the plugin API internals. If there is a better hook for this or a documented contract I missed, let me know.
What I found
OpenCode subagents share their parent's plugin hooks. The
tool.execute.afterhook fires for every subagent session. It receives the session ID and the tool output. And it can mutate that output before the model sees it.This means you can splice a correction into a running subagent's context mid-turn. No daemon, no polling, no bash tool that the model might skip. The hook fires deterministically.
How it works
The operator writes a message to a maildir spool (
new/directory). The plugin hook, running on everytool.execute.after, checks the spool, atomically moves the message tocur/, and appends it to the tool output text. The language model sees it on its next reasoning step:The maildir gives crash-safe delivery without any locks. A message being written stays in
tmp/. Once fully written and synced, an atomic rename puts it innew/. The reader atomically renames it tocur/when delivered. Kill the process at any point and you either get the message or you do not. Partial reads are impossible.The code
Working implementation at https://github.com/semanticRig/cross-session-agent-messaging
About 500 lines total: a Python core library for the spool, a TypeScript plugin for the injection hooks, and MCP server wrappers for the tools. Tested on OpenCode 1.18.15 with 10 parallel subagents.
Honest limitations
new/and the move tocur/can double-deliver. The message ID handles deduplication.Why this matters
Claude Code added agent teams in 2.1.224. You can correct a subagent mid-task from another terminal. OpenCode has no equivalent. But the plugin system makes it possible to build one with remarkably little code.
I wrote up the technical details and the discovery process at the repo linked above. Would love feedback, especially from anyone who has worked with the plugin API internals. If there is a better hook for this or a documented contract I missed, let me know.