opencode-middleware
v0.1.2
Published
Composable middleware pipeline for OpenCode subagent tasks
Maintainers
Readme
Middleware
An OpenCode plugin that wraps subagent task execution with composable middleware. Middleware is declared on the agent being executed, not the caller. It shapes input before the task runs and output after it completes.
How It Works
The plugin registers two hooks with OpenCode:
tool.execute.before— fires before a task tool executes- Reads the agent name from
args.subagent_type - Looks up which middleware the agent declares
- Runs before hooks sequentially (outside-in: A → B → C)
- Before hooks can mutate
argsto shape the task's input
- Reads the agent name from
tool.execute.after— fires after a task tool completes- Looks up the middleware established during the before phase (correlated by
callID) - Runs after hooks sequentially in reverse order (inside-out: C → B → A)
- After hooks can mutate
title,output, andmetadata
- Looks up the middleware established during the before phase (correlated by
For middleware declared [A, B, C] on an agent:
A.before → B.before → C.before
← task executes →
C.after → B.after → A.afterCallers never know middleware ran. They see the final output.
Writing Middleware
Each middleware is a .ts file with a default export containing optional before and after hooks:
export default {
after: async (ctx, result) => {
const checks = (ctx.config.checks ?? []) as string[]
const findings = checks.map((check) => ({
check,
mentioned: result.output.toLowerCase().includes(check.toLowerCase()),
}))
const passed = findings.every((finding) => finding.mentioned)
const summary = passed ? "All checks passed." : "Some checks missing."
result.output = summary + "\n\n" + result.output
},
}The filename becomes the registry key. auditor.ts registers as "auditor".
Hook Signatures
interface MiddlewareModule {
before?: (ctx: MiddlewareContext, args: { args: unknown }) => Promise<void>
after?: (ctx: MiddlewareContext, result: { title: string; output: string; metadata: unknown }) => Promise<void>
}Both hooks mutate their second argument in place.
ctx carries:
client— the OpenCode SDK clientsession.id— the OpenCode session IDsubtask.agent— the agent being executed (e.g.,"red")subtask.prompt— what the agent was asked to doconfig— per-entry config from agent frontmatter
What Middleware Can Do
Before hooks shape input:
- Inject additional instructions into args
- Throw to abort execution
After hooks shape output:
- Append audit results or summaries to output
- Rewrite the title
- Attach metadata
Configuring Agents
Middleware is declared in agent frontmatter under options.middleware:
# red.md
---
description: Write one failing test
mode: subagent
options:
middleware:
- run: auditor
config:
checks:
- aaa
- single_test
---run— middleware name, resolved from the registry by filenameconfig— arbitrary config injected asctx.config- Order matters — determines before/after execution order
Directory Layout
Middleware files live on disk. By default, the plugin scans ~/.config/opencode/middleware/.
Directories are scanned recursively — organize middleware into subdirectories as you see fit:
~/.config/opencode/middleware/
auditing/
auditor.ts → registered as "auditor"
compliance.ts → registered as "compliance"
distilling/
distiller.ts → registered as "distiller"
learner.ts → registered as "learner"The filename (without .ts) becomes the registry key regardless of subdirectory depth. Duplicate basenames across subdirectories are an error.
To scan additional directories, create ~/.config/opencode/middleware.json:
{
"directories": [
"~/.config/opencode/middleware",
"/path/to/project/middleware"
]
}directories replaces the default entirely. Include the default path if you still want it scanned.
Local Setup
npm install
npm test # unit tests
npm run test:integration # unit + integration tests
npm run build # compile to dist/Register with OpenCode
Add the plugin to your project's opencode.json:
{
"plugin": ["/absolute/path/to/middleware"]
}OpenCode runs on Bun, which imports TypeScript directly — no build step needed for local use.
Examples
The examples/ directory contains working samples:
examples/middleware/auditor.ts— after hook that checks output against configurable criteria and prepends an audit summaryexamples/middleware/tag.ts— after hook that appends a configurable label to outputexamples/agents/red.md— agent frontmatter declaring middleware with per-entry configexamples/opencode.json— plugin registrationexamples/middleware.json— config override pointing at the examples directory
Architecture
src/
index.ts — entry point (default export, wires production deps)
chain/
runner.ts — BeforeHook, AfterHook, MiddlewareModule, runBefore, runAfter
builder.ts — ChainEntry, buildChain (binds config + context to hooks)
registry/
registry.ts — Registry, createRegistry (name → MiddlewareModule lookup)
loader/
loader.ts — FileSystem, ModuleLoader, loadMiddleware (directory scanning)
config.ts — ConfigFile, MiddlewareConfig, readConfig
plugin/
hook.ts — createMiddlewareHooks (before + after handlers, callID correlation)
agent-provider.ts — createAgentProvider (agent name → middleware config)
plugin.ts — createPlugin (top-level wiring)
platform/
node.ts — Node.js implementations of FileSystem, ModuleLoader, ConfigFilePluginDependencies (FileSystem, ModuleLoader, ConfigFile) are injected interfaces. Production implementations in src/platform/node.ts wrap Node's fs.readdir, import(), and fs.readFile. Tests substitute fakes.
