opencode-command-hooks 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +796 -0
- package/dist/config/agent.d.ts +82 -0
- package/dist/config/agent.d.ts.map +1 -0
- package/dist/config/agent.js +145 -0
- package/dist/config/agent.js.map +1 -0
- package/dist/config/global.d.ts +36 -0
- package/dist/config/global.d.ts.map +1 -0
- package/dist/config/global.js +219 -0
- package/dist/config/global.js.map +1 -0
- package/dist/config/markdown.d.ts +119 -0
- package/dist/config/markdown.d.ts.map +1 -0
- package/dist/config/markdown.js +373 -0
- package/dist/config/markdown.js.map +1 -0
- package/dist/config/merge.d.ts +67 -0
- package/dist/config/merge.d.ts.map +1 -0
- package/dist/config/merge.js +192 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/execution/shell.d.ts +55 -0
- package/dist/execution/shell.d.ts.map +1 -0
- package/dist/execution/shell.js +187 -0
- package/dist/execution/shell.js.map +1 -0
- package/dist/execution/template.d.ts +55 -0
- package/dist/execution/template.d.ts.map +1 -0
- package/dist/execution/template.js +106 -0
- package/dist/execution/template.js.map +1 -0
- package/dist/executor.d.ts +54 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +314 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +359 -0
- package/dist/index.js.map +1 -0
- package/dist/logging.d.ts +24 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +57 -0
- package/dist/logging.js.map +1 -0
- package/dist/schemas.d.ts +425 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +150 -0
- package/dist/schemas.js.map +1 -0
- package/dist/types/hooks.d.ts +635 -0
- package/dist/types/hooks.d.ts.map +1 -0
- package/dist/types/hooks.js +12 -0
- package/dist/types/hooks.js.map +1 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
# 🪝OpenCode Command Hooks
|
|
2
|
+
|
|
3
|
+
Attach shell commands to agent, tool, and session lifecycles using JSON/YAML configuration. Execute commands automatically without consuming tokens or requiring agent interaction.
|
|
4
|
+
|
|
5
|
+
**Quick Win:** Make your engineer subagent self-validating with 15 lines of config (no TypeScript, no rebuilds, zero tokens):
|
|
6
|
+
|
|
7
|
+
````jsonc
|
|
8
|
+
{
|
|
9
|
+
"tool": [
|
|
10
|
+
{
|
|
11
|
+
"id": "auto-validate",
|
|
12
|
+
"when": {
|
|
13
|
+
"phase": "after",
|
|
14
|
+
"tool": "task",
|
|
15
|
+
"toolArgs": { "subagent_type": "engineer" },
|
|
16
|
+
},
|
|
17
|
+
"run": ["npm run typecheck", "npm test"],
|
|
18
|
+
"inject": "✅ Validation: {exitCode, select, 0 {Passed} other {Failed - fix errors below}}\n```\n{stdout}\n```",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
````
|
|
23
|
+
|
|
24
|
+
Every time the engineer finishes, tests run automatically and results flow back to the orchestrator. Failed validations trigger self-healing—no manual intervention, no token costs.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Simplified Agent Markdown Hooks
|
|
29
|
+
|
|
30
|
+
Define hooks directly in your agent's markdown file for maximum simplicity. No need for global configs, `id` fields, `when` clauses, or `tool` specifications—everything is auto-configured when you use subagents via the `task` tool.
|
|
31
|
+
|
|
32
|
+
### Quick Example
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
---
|
|
36
|
+
description: My agent
|
|
37
|
+
mode: subagent
|
|
38
|
+
hooks:
|
|
39
|
+
before:
|
|
40
|
+
- run: "echo 'Starting...'"
|
|
41
|
+
after:
|
|
42
|
+
- run: ["npm run typecheck", "npm run lint"]
|
|
43
|
+
inject: "Results:\n{stdout}"
|
|
44
|
+
- run: "npm test"
|
|
45
|
+
toast:
|
|
46
|
+
message: "Tests {exitCode, select, 0 {passed} other {failed}}"
|
|
47
|
+
---
|
|
48
|
+
# Your agent markdown content here
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### How It Works
|
|
52
|
+
|
|
53
|
+
1. **Automatic Targeting**: Hooks defined in agent markdown automatically apply when that subagent is invoked via the `task` tool
|
|
54
|
+
2. **Simplified Syntax**: No `id`, `when`, or `tool` fields needed—everything is inferred from context
|
|
55
|
+
3. **Before/After Hooks**: Use `before` hooks for setup/preparation and `after` hooks for validation/cleanup
|
|
56
|
+
4. **Dual Location Support**: Works with both:
|
|
57
|
+
- `.opencode/agent/*.md` (project-level agents)
|
|
58
|
+
- `~/.config/opencode/agent/*.md` (user-level agents)
|
|
59
|
+
|
|
60
|
+
### Simplified vs Global Config Format
|
|
61
|
+
|
|
62
|
+
**Global Config (verbose):**
|
|
63
|
+
|
|
64
|
+
```jsonc
|
|
65
|
+
{
|
|
66
|
+
"tool": [
|
|
67
|
+
{
|
|
68
|
+
"id": "validate-engineer",
|
|
69
|
+
"when": {
|
|
70
|
+
"phase": "after",
|
|
71
|
+
"tool": "task",
|
|
72
|
+
"toolArgs": { "subagent_type": "engineer" },
|
|
73
|
+
},
|
|
74
|
+
"run": ["npm run typecheck", "npm test"],
|
|
75
|
+
"inject": "Validation: {exitCode, select, 0 {✓ Passed} other {✗ Failed}}",
|
|
76
|
+
"toast": {
|
|
77
|
+
"title": "Validation Complete",
|
|
78
|
+
"message": "{exitCode, select, 0 {✓ Passed} other {✗ Failed}}",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Agent Markdown (simplified):**
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
hooks:
|
|
89
|
+
after:
|
|
90
|
+
- run: ["npm run typecheck", "npm test"]
|
|
91
|
+
inject: "Validation: {exitCode, select, 0 {✓ Passed} other {✗ Failed}}"
|
|
92
|
+
toast:
|
|
93
|
+
message: "{exitCode, select, 0 {✓ Passed} other {✗ Failed}}"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Reduction: 60% less boilerplate!**
|
|
97
|
+
|
|
98
|
+
### Hook Configuration Options
|
|
99
|
+
|
|
100
|
+
| Option | Type | Required | Description |
|
|
101
|
+
| -------- | ---------------------- | -------- | --------------------------------------------------- |
|
|
102
|
+
| `run` | `string` \| `string[]` | ✅ Yes | Command(s) to execute |
|
|
103
|
+
| `inject` | `string` | ❌ No | Message to inject into session (supports templates) |
|
|
104
|
+
| `toast` | `object` | ❌ No | Toast notification configuration |
|
|
105
|
+
|
|
106
|
+
### Toast Configuration
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
toast:
|
|
110
|
+
message: "Build {exitCode, select, 0 {succeeded} other {failed}}"
|
|
111
|
+
variant: "{exitCode, select, 0 {success} other {error}}" # info, success, warning, error
|
|
112
|
+
duration: 5000 # milliseconds (optional)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Template Variables
|
|
116
|
+
|
|
117
|
+
Agent markdown hooks support the same template variables as global config:
|
|
118
|
+
|
|
119
|
+
- `{stdout}` - Command stdout (truncated to 4000 chars)
|
|
120
|
+
- `{stderr}` - Command stderr (truncated to 4000 chars)
|
|
121
|
+
- `{exitCode}` - Command exit code
|
|
122
|
+
- `{cmd}` - Executed command
|
|
123
|
+
|
|
124
|
+
### Complete Example
|
|
125
|
+
|
|
126
|
+
````yaml
|
|
127
|
+
---
|
|
128
|
+
description: Engineer Agent
|
|
129
|
+
mode: subagent
|
|
130
|
+
hooks:
|
|
131
|
+
before:
|
|
132
|
+
- run: "echo '🚀 Engineer agent starting...'"
|
|
133
|
+
after:
|
|
134
|
+
- run: ["npm run typecheck", "npm run lint"]
|
|
135
|
+
inject: |
|
|
136
|
+
## Validation Results
|
|
137
|
+
|
|
138
|
+
**TypeCheck:** {exitCode, select, 0 {✓ Passed} other {✗ Failed}}
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
{stdout}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
{exitCode, select, 0 {} other {⚠️ Please fix validation errors before proceeding.}}
|
|
145
|
+
toast:
|
|
146
|
+
message: "TypeCheck & Lint: {exitCode, select, 0 {✓ Passed} other {✗ Failed}}"
|
|
147
|
+
variant: "{exitCode, select, 0 {success} other {error}}"
|
|
148
|
+
- run: "npm test -- --coverage --passWithNoTests"
|
|
149
|
+
inject: "Test Coverage: {stdout}%"
|
|
150
|
+
toast:
|
|
151
|
+
message: "Tests {exitCode, select, 0 {✓ Passed} other {✗ Failed}}"
|
|
152
|
+
variant: "{exitCode, select, 0 {success} other {error}}"
|
|
153
|
+
---
|
|
154
|
+
# Engineer Agent
|
|
155
|
+
Focus on implementing features with tests and proper error handling.
|
|
156
|
+
````
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Why?
|
|
161
|
+
|
|
162
|
+
**The Problem:** You want your engineer/validator subagents to automatically run tests/linters and self-heal when validation fails—but asking agents to run validation costs tokens, isn't guaranteed, and requires complex native plugin code with manual error handling.
|
|
163
|
+
|
|
164
|
+
**The Solution:** This plugin lets you attach validation commands to subagent completions via simple config. Results automatically inject back to the orchestrator, enabling autonomous quality gates with zero tokens spent.
|
|
165
|
+
|
|
166
|
+
### 1. Zero-Token Automation
|
|
167
|
+
|
|
168
|
+
Custom OpenCode plugins require TypeScript setup, manual error handling, and build processes. For routine automation tasks, this creates unnecessary complexity. This plugin provides shell hook functionality through configuration files—no tokens spent prompting agents to run repetitive commands.
|
|
169
|
+
|
|
170
|
+
**Native Plugin (Requires TypeScript):**
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// .opencode/plugin/my-plugin.ts
|
|
174
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
175
|
+
|
|
176
|
+
export const MyPlugin: Plugin = async ({ $ }) => {
|
|
177
|
+
return {
|
|
178
|
+
"tool.execute.after": async (input) => {
|
|
179
|
+
if (input.tool === "write") {
|
|
180
|
+
try {
|
|
181
|
+
await $`npm run lint`;
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error(e); // UI spam or crashes
|
|
184
|
+
// Agent won't see what failed unless you inject it back
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**This Plugin (Configuration-Based):**
|
|
193
|
+
|
|
194
|
+
```jsonc
|
|
195
|
+
{
|
|
196
|
+
"tool": [
|
|
197
|
+
{
|
|
198
|
+
"id": "lint-ts",
|
|
199
|
+
"when": { "tool": "write" },
|
|
200
|
+
"run": ["npm run lint"],
|
|
201
|
+
"inject": {
|
|
202
|
+
"as": "system",
|
|
203
|
+
"template": "Lint results:\n{stdout}\n{stderr}",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 2. Automatic Context Injection
|
|
211
|
+
|
|
212
|
+
Command output returns to the agent automatically without manual SDK calls. The agent can see and react to errors, warnings, or other results:
|
|
213
|
+
|
|
214
|
+
```jsonc
|
|
215
|
+
{
|
|
216
|
+
"tool": [
|
|
217
|
+
{
|
|
218
|
+
"id": "typecheck",
|
|
219
|
+
"when": { "tool": "write", "toolArgs": { "path": "*.ts" } },
|
|
220
|
+
"run": ["npm run typecheck"],
|
|
221
|
+
"inject": {
|
|
222
|
+
"as": "system",
|
|
223
|
+
"template": "TypeScript check:\n{stdout}\n{stderr}",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
For background tasks where the agent doesn't need context, use toast notifications instead:
|
|
231
|
+
|
|
232
|
+
```jsonc
|
|
233
|
+
{
|
|
234
|
+
"tool": [
|
|
235
|
+
{
|
|
236
|
+
"id": "build-status",
|
|
237
|
+
"when": { "tool": "write", "phase": "after" },
|
|
238
|
+
"run": ["npm run build"],
|
|
239
|
+
"toast": {
|
|
240
|
+
"title": "Build Status",
|
|
241
|
+
"message": "Build {exitCode, select, 0 {succeeded} other {failed}}",
|
|
242
|
+
"variant": "{exitCode, select, 0 {success} other {error}}",
|
|
243
|
+
"duration": 5000,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 3. Filter Tools by Any Argument
|
|
251
|
+
|
|
252
|
+
Run different hooks based on any tool argument—not just task subagent types. Filter by file paths, API endpoints, model names, or custom parameters.
|
|
253
|
+
|
|
254
|
+
```jsonc
|
|
255
|
+
// Filter by multiple subagent types
|
|
256
|
+
{
|
|
257
|
+
"when": {
|
|
258
|
+
"tool": "task",
|
|
259
|
+
"toolArgs": { "subagent_type": ["validator", "reviewer", "tester"] }
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Filter write tool by file extension
|
|
264
|
+
{
|
|
265
|
+
"when": {
|
|
266
|
+
"tool": "write",
|
|
267
|
+
"toolArgs": { "path": "*.test.ts" }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Filter by API endpoint
|
|
272
|
+
{
|
|
273
|
+
"when": {
|
|
274
|
+
"tool": "fetch",
|
|
275
|
+
"toolArgs": { "url": "*/api/validate" }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### 4. Reliable Execution
|
|
281
|
+
|
|
282
|
+
- **Non-blocking**: Hook failures don't crash the agent
|
|
283
|
+
- **Automatic error handling**: Failed hooks inject error messages automatically
|
|
284
|
+
- **Memory safe**: Output truncated to prevent memory issues
|
|
285
|
+
- **Sequential execution**: Commands run in order, even if earlier ones fail
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Features
|
|
290
|
+
|
|
291
|
+
- 🔔 **Toast Notifications** - Non-blocking user feedback with customizable titles, messages, and variants
|
|
292
|
+
- 🎯 **Precise Filtering** - Filter by tool name, agent, phase, slash command, or ANY tool argument
|
|
293
|
+
- 📊 **Complete Template System** - Access to `{id}`, `{agent}`, `{tool}`, `{cmd}`, `{stdout}`, `{stderr}`, `{exitCode}`
|
|
294
|
+
- 🔄 **Multiple Event Types** - `tool.execute.before`, `tool.execute.after`, `tool.result`, `session.start`, `session.idle`
|
|
295
|
+
- 🧩 **Flexible Configuration** - Global configs in `.opencode/command-hooks.jsonc` or markdown frontmatter
|
|
296
|
+
- ⚡ **Zero-Token Automation** - Guaranteed execution without spending tokens on agent prompts
|
|
297
|
+
- 🛡️ **Bulletproof Error Handling** - Graceful failures that never crash the agent
|
|
298
|
+
- 🐛 **Debug Mode** - Detailed logging with `OPENCODE_HOOKS_DEBUG=1`
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Installation
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
opencode install opencode-command-hooks
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Configuration
|
|
311
|
+
|
|
312
|
+
### Global Configuration
|
|
313
|
+
|
|
314
|
+
Create `.opencode/command-hooks.jsonc` in your project root:
|
|
315
|
+
|
|
316
|
+
```jsonc
|
|
317
|
+
{
|
|
318
|
+
"tool": [
|
|
319
|
+
// Your tool hooks here
|
|
320
|
+
],
|
|
321
|
+
"session": [
|
|
322
|
+
// Your session hooks here
|
|
323
|
+
],
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Markdown Frontmatter
|
|
328
|
+
|
|
329
|
+
Override global settings in individual markdown files:
|
|
330
|
+
|
|
331
|
+
```markdown
|
|
332
|
+
---
|
|
333
|
+
opencode-hooks:
|
|
334
|
+
tool:
|
|
335
|
+
- id: custom-hook
|
|
336
|
+
when: { tool: "write" }
|
|
337
|
+
run: ["echo 'File modified'"]
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
# Your markdown content
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Configuration Precedence
|
|
344
|
+
|
|
345
|
+
1. **Markdown hooks** override global hooks with the same ID
|
|
346
|
+
2. **Global hooks** are loaded from `.opencode/command-hooks.jsonc`
|
|
347
|
+
3. **Duplicate IDs** within the same source are errors
|
|
348
|
+
4. **Caching** prevents repeated file reads for performance
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Examples
|
|
353
|
+
|
|
354
|
+
### 🚀 Power User Example: Autonomous Quality Gates
|
|
355
|
+
|
|
356
|
+
This example demonstrates the plugin's killer feature: **automatic validation injection after subagent work**. Unlike native plugins that require complex TypeScript and manual result handling, this achieves enterprise-grade quality gates with pure configuration.
|
|
357
|
+
|
|
358
|
+
````jsonc
|
|
359
|
+
{
|
|
360
|
+
"tool": [
|
|
361
|
+
{
|
|
362
|
+
"id": "validate-engineer-work",
|
|
363
|
+
"when": {
|
|
364
|
+
"phase": "after",
|
|
365
|
+
"tool": "task",
|
|
366
|
+
"toolArgs": { "subagent_type": ["engineer", "debugger"] },
|
|
367
|
+
},
|
|
368
|
+
"run": [
|
|
369
|
+
"npm run typecheck",
|
|
370
|
+
"npm run lint",
|
|
371
|
+
"npm test -- --coverage --passWithNoTests",
|
|
372
|
+
],
|
|
373
|
+
"inject": "🔍 Validation Results:\n\n**TypeCheck:** {exitCode, select, 0 {✓ Passed} other {✗ Failed}}\n\n```\n{stdout}\n```\n\n{exitCode, select, 0 {} other {⚠️ The code you just wrote has validation errors. Please fix them before proceeding.}}",
|
|
374
|
+
"toast": {
|
|
375
|
+
"title": "Code Validation",
|
|
376
|
+
"message": "{exitCode, select, 0 {All checks passed ✓} other {Validation failed - check errors}}",
|
|
377
|
+
"variant": "{exitCode, select, 0 {success} other {error}}",
|
|
378
|
+
"duration": 5000,
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"id": "verify-test-coverage",
|
|
383
|
+
"when": {
|
|
384
|
+
"phase": "after",
|
|
385
|
+
"tool": "task",
|
|
386
|
+
"toolArgs": { "subagent_type": "engineer" },
|
|
387
|
+
},
|
|
388
|
+
"run": [
|
|
389
|
+
"npm test -- --coverage --json > coverage.json && node -p 'JSON.parse(require(\"fs\").readFileSync(\"coverage.json\")).coverageMap.total.lines.pct'",
|
|
390
|
+
],
|
|
391
|
+
"inject": "📊 Test Coverage: {stdout}%\n\n{stdout, select, ^[89]\\d|100$ {} other {⚠️ Coverage is below 80%. Please add more tests.}}",
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
}
|
|
395
|
+
````
|
|
396
|
+
|
|
397
|
+
**What makes this powerful:**
|
|
398
|
+
|
|
399
|
+
1. **Zero-Token Enforcement** - Quality gates run automatically after engineer/debugger subagents without consuming tokens to prompt validation
|
|
400
|
+
2. **Intelligent Filtering** - Uses `toolArgs.subagent_type` to target specific subagents (impossible with native plugins without SDK access)
|
|
401
|
+
3. **Context Injection** - Validation results automatically flow back to the orchestrator/agent, enabling self-healing workflows
|
|
402
|
+
4. **Non-Blocking** - Failed validations don't crash the session; the agent sees errors and can fix them
|
|
403
|
+
5. **Dual Feedback** - Users get instant toast notifications while agents receive detailed error context
|
|
404
|
+
6. **Sequential Commands** - Multiple validation steps run in order, even if earlier ones fail
|
|
405
|
+
7. **Template Power** - Conditional messages using ICU MessageFormat syntax (`{exitCode, select, ...}`)
|
|
406
|
+
|
|
407
|
+
**Real-world impact:** A single hook configuration replaces 50+ lines of TypeScript plugin code with error handling, session.prompt calls, and manual filtering logic. The agent becomes self-validating without you spending a single token to ask it to run tests.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
### Basic Examples
|
|
412
|
+
|
|
413
|
+
#### Auto-Verify Subagent Work
|
|
414
|
+
|
|
415
|
+
````jsonc
|
|
416
|
+
{
|
|
417
|
+
"tool": [
|
|
418
|
+
{
|
|
419
|
+
"id": "verify-subagent-work",
|
|
420
|
+
"when": {
|
|
421
|
+
"phase": "after",
|
|
422
|
+
"tool": ["task"],
|
|
423
|
+
},
|
|
424
|
+
"run": ["npm test"],
|
|
425
|
+
"inject": "Test Runner:\nExit Code: {exitCode}\n\nOutput:\n```\n{stdout}\n```\n\nIf tests failed, please fix them before proceeding.",
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
}
|
|
429
|
+
````
|
|
430
|
+
|
|
431
|
+
#### Multi-Stage Validation Pipeline
|
|
432
|
+
|
|
433
|
+
````jsonc
|
|
434
|
+
{
|
|
435
|
+
"tool": [
|
|
436
|
+
{
|
|
437
|
+
"id": "validator-security-scan",
|
|
438
|
+
"when": {
|
|
439
|
+
"phase": "after",
|
|
440
|
+
"tool": "task",
|
|
441
|
+
"toolArgs": { "subagent_type": "validator" },
|
|
442
|
+
},
|
|
443
|
+
"run": [
|
|
444
|
+
"npm audit --audit-level=moderate",
|
|
445
|
+
"git diff --name-only | xargs grep -l 'API_KEY\\|SECRET\\|PASSWORD' || true",
|
|
446
|
+
],
|
|
447
|
+
"inject": "🔒 Security Scan:\n\n**Audit:** {exitCode, select, 0 {No vulnerabilities} other {⚠️ Vulnerabilities found}}\n\n```\n{stdout}\n```\n\nPlease address security issues before deployment.",
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
}
|
|
451
|
+
````
|
|
452
|
+
|
|
453
|
+
#### Enforce Linting on File Edits
|
|
454
|
+
|
|
455
|
+
```jsonc
|
|
456
|
+
{
|
|
457
|
+
"tool": [
|
|
458
|
+
{
|
|
459
|
+
"id": "lint-on-save",
|
|
460
|
+
"when": {
|
|
461
|
+
"phase": "after",
|
|
462
|
+
"tool": ["write"],
|
|
463
|
+
},
|
|
464
|
+
"run": ["npm run lint -- --fix"],
|
|
465
|
+
"inject": "Linting auto-fix results: {stdout}",
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Advanced Examples
|
|
472
|
+
|
|
473
|
+
#### Toast Notifications for Build Status
|
|
474
|
+
|
|
475
|
+
````jsonc
|
|
476
|
+
{
|
|
477
|
+
"tool": [
|
|
478
|
+
{
|
|
479
|
+
"id": "build-notification",
|
|
480
|
+
"when": {
|
|
481
|
+
"phase": "after",
|
|
482
|
+
"tool": ["write"],
|
|
483
|
+
"toolArgs": { "path": "*.ts" },
|
|
484
|
+
},
|
|
485
|
+
"run": ["npm run build"],
|
|
486
|
+
"toast": {
|
|
487
|
+
"title": "TypeScript Build",
|
|
488
|
+
"message": "Build {exitCode, select, 0 {succeeded ✓} other {failed ✗}}",
|
|
489
|
+
"variant": "{exitCode, select, 0 {success} other {error}}",
|
|
490
|
+
"duration": 3000,
|
|
491
|
+
},
|
|
492
|
+
"inject": {
|
|
493
|
+
"as": "system",
|
|
494
|
+
"template": "Build output:\n```\n{stdout}\n```",
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
}
|
|
499
|
+
````
|
|
500
|
+
|
|
501
|
+
#### Filter by Multiple Tool Arguments
|
|
502
|
+
|
|
503
|
+
```jsonc
|
|
504
|
+
{
|
|
505
|
+
"tool": [
|
|
506
|
+
// Run for specific file types
|
|
507
|
+
{
|
|
508
|
+
"id": "test-js-files",
|
|
509
|
+
"when": {
|
|
510
|
+
"tool": "write",
|
|
511
|
+
"toolArgs": {
|
|
512
|
+
"path": ["*.js", "*.jsx", "*.ts", "*.tsx"],
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
"run": ["npm test -- --testPathPattern={toolArgs.path}"],
|
|
516
|
+
},
|
|
517
|
+
// Filter by multiple subagent types
|
|
518
|
+
{
|
|
519
|
+
"id": "validate-subagents",
|
|
520
|
+
"when": {
|
|
521
|
+
"tool": "task",
|
|
522
|
+
"toolArgs": {
|
|
523
|
+
"subagent_type": ["validator", "reviewer", "tester"],
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
"run": ["echo 'Validating {toolArgs.subagent_type} subagent'"],
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### Handle Async Tool Completion
|
|
533
|
+
|
|
534
|
+
```jsonc
|
|
535
|
+
{
|
|
536
|
+
"tool": [
|
|
537
|
+
{
|
|
538
|
+
"id": "task-complete",
|
|
539
|
+
"when": {
|
|
540
|
+
"event": "tool.result",
|
|
541
|
+
"tool": ["task"],
|
|
542
|
+
"toolArgs": { "subagent_type": "code-writer" },
|
|
543
|
+
},
|
|
544
|
+
"run": ["npm run validate-changes"],
|
|
545
|
+
"toast": {
|
|
546
|
+
"title": "Code Writer Complete",
|
|
547
|
+
"message": "Validation: {exitCode, select, 0 {Passed} other {Failed}}",
|
|
548
|
+
"variant": "{exitCode, select, 0 {success} other {warning}}",
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
#### Session Lifecycle Hooks
|
|
556
|
+
|
|
557
|
+
```jsonc
|
|
558
|
+
{
|
|
559
|
+
"session": [
|
|
560
|
+
{
|
|
561
|
+
"id": "session-start",
|
|
562
|
+
"when": { "event": "session.start" },
|
|
563
|
+
"run": ["echo 'New session started'"],
|
|
564
|
+
"toast": {
|
|
565
|
+
"title": "Session Started",
|
|
566
|
+
"message": "Ready to assist!",
|
|
567
|
+
"variant": "info",
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
"id": "session-idle",
|
|
572
|
+
"when": { "event": "session.idle" },
|
|
573
|
+
"run": ["echo 'Session idle'"],
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## Template Placeholders
|
|
582
|
+
|
|
583
|
+
All templates support these placeholders:
|
|
584
|
+
|
|
585
|
+
| Placeholder | Description | Example |
|
|
586
|
+
| ------------ | ---------------------------------------- | -------------------------- |
|
|
587
|
+
| `{id}` | Hook ID | `lint-ts` |
|
|
588
|
+
| `{agent}` | Calling agent name | `orchestrator` |
|
|
589
|
+
| `{tool}` | Tool name | `write` |
|
|
590
|
+
| `{cmd}` | Executed command | `npm run lint` |
|
|
591
|
+
| `{stdout}` | Command stdout (truncated to 4000 chars) | `Linting complete` |
|
|
592
|
+
| `{stderr}` | Command stderr (truncated to 4000 chars) | `Error: missing semicolon` |
|
|
593
|
+
| `{exitCode}` | Command exit code | `0` or `1` |
|
|
594
|
+
|
|
595
|
+
**Advanced Usage:**
|
|
596
|
+
|
|
597
|
+
```jsonc
|
|
598
|
+
{
|
|
599
|
+
"inject": {
|
|
600
|
+
"template": "Command '{cmd}' exited with code {exitCode}\n\nStdout:\n{stdout}\n\nStderr:\n{stderr}",
|
|
601
|
+
},
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## Debugging
|
|
608
|
+
|
|
609
|
+
Enable detailed debug logging:
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
export OPENCODE_HOOKS_DEBUG=1
|
|
613
|
+
opencode start
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
This logs:
|
|
617
|
+
|
|
618
|
+
- Command execution details
|
|
619
|
+
- Template interpolation
|
|
620
|
+
- Hook matching logic
|
|
621
|
+
- Error handling
|
|
622
|
+
|
|
623
|
+
**Example Debug Output:**
|
|
624
|
+
|
|
625
|
+
```
|
|
626
|
+
[DEBUG] Hook matched: lint-ts
|
|
627
|
+
[DEBUG] Executing: npm run lint
|
|
628
|
+
[DEBUG] Template interpolated: Exit code: 0
|
|
629
|
+
[DEBUG] Toast shown: Lint Complete
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
## Event Types
|
|
635
|
+
|
|
636
|
+
### Tool Events
|
|
637
|
+
|
|
638
|
+
- **`tool.execute.before`** - Before tool execution
|
|
639
|
+
- **`tool.execute.after`** - After tool execution
|
|
640
|
+
- **`tool.result`** - When async tools complete (task, firecrawl, etc.)
|
|
641
|
+
|
|
642
|
+
### Session Events
|
|
643
|
+
|
|
644
|
+
- **`session.start`** - New session started
|
|
645
|
+
- **`session.idle`** - Session became idle
|
|
646
|
+
- **`session.end`** - Session ended
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Tool vs Session Hooks
|
|
651
|
+
|
|
652
|
+
### Tool Hooks
|
|
653
|
+
|
|
654
|
+
- Run before/after specific tool executions
|
|
655
|
+
- Can filter by tool name, calling agent, slash command, tool arguments
|
|
656
|
+
- Access to tool-specific context (tool name, call ID, args)
|
|
657
|
+
- **Best for**: Linting after writes, tests after code changes, validation
|
|
658
|
+
|
|
659
|
+
### Session Hooks
|
|
660
|
+
|
|
661
|
+
- Run on session lifecycle events
|
|
662
|
+
- Can only filter by agent name (session events lack tool context)
|
|
663
|
+
- **Best for**: Bootstrapping, cleanup, periodic checks
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## Native Plugin vs This Plugin
|
|
668
|
+
|
|
669
|
+
**The Real Difference:** The Power User Example above would require this native plugin implementation:
|
|
670
|
+
|
|
671
|
+
**Native Plugin: 73 lines of TypeScript with manual everything**
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
675
|
+
|
|
676
|
+
export const ValidationPlugin: Plugin = async ({ $, client }) => {
|
|
677
|
+
return {
|
|
678
|
+
"tool.execute.after": async (input) => {
|
|
679
|
+
if (input.tool !== "task") return;
|
|
680
|
+
|
|
681
|
+
// No way to filter by toolArgs.subagent_type without complex parsing
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
const results: string[] = [];
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
const typecheck = await $`npm run typecheck`.text();
|
|
688
|
+
results.push(`TypeCheck: ${typecheck}`);
|
|
689
|
+
} catch (e: any) {
|
|
690
|
+
results.push(`TypeCheck failed: ${e.stderr || e.message}`);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
try {
|
|
694
|
+
const lint = await $`npm run lint`.text();
|
|
695
|
+
results.push(`Lint: ${lint}`);
|
|
696
|
+
} catch (e: any) {
|
|
697
|
+
results.push(`Lint failed: ${e.stderr || e.message}`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
try {
|
|
701
|
+
const test = await $`npm test -- --coverage --passWithNoTests`.text();
|
|
702
|
+
results.push(`Tests: ${test}`);
|
|
703
|
+
} catch (e: any) {
|
|
704
|
+
results.push(`Tests failed: ${e.stderr || e.message}`);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const output = results.join("\n\n");
|
|
708
|
+
const exitCode = results.some((r) => r.includes("failed")) ? 1 : 0;
|
|
709
|
+
const message = `🔍 Validation Results:\n\n${output}\n\n${
|
|
710
|
+
exitCode !== 0
|
|
711
|
+
? "⚠️ The code you just wrote has validation errors. Please fix them before proceeding."
|
|
712
|
+
: ""
|
|
713
|
+
}`;
|
|
714
|
+
|
|
715
|
+
await client.session.prompt({
|
|
716
|
+
sessionID: input.sessionID,
|
|
717
|
+
message,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
console.log(
|
|
721
|
+
exitCode === 0 ? "✓ All checks passed" : "✗ Validation failed",
|
|
722
|
+
);
|
|
723
|
+
} catch (e) {
|
|
724
|
+
console.error("Validation hook failed:", e);
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
};
|
|
728
|
+
};
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
Problems: Can't filter by subagent_type, manual error handling for each command, manual template building, manual session.prompt calls, no toast notifications, must rebuild after changes.
|
|
732
|
+
|
|
733
|
+
**This Plugin: 15 lines of JSON**
|
|
734
|
+
|
|
735
|
+
````jsonc
|
|
736
|
+
{
|
|
737
|
+
"id": "validate-engineer-work",
|
|
738
|
+
"when": {
|
|
739
|
+
"phase": "after",
|
|
740
|
+
"tool": "task",
|
|
741
|
+
"toolArgs": { "subagent_type": ["engineer", "debugger"] },
|
|
742
|
+
},
|
|
743
|
+
"run": [
|
|
744
|
+
"npm run typecheck",
|
|
745
|
+
"npm run lint",
|
|
746
|
+
"npm test -- --coverage --passWithNoTests",
|
|
747
|
+
],
|
|
748
|
+
"inject": "🔍 Validation Results:\n\n**TypeCheck:** {exitCode, select, 0 {✓ Passed} other {✗ Failed}}\n\n```\n{stdout}\n```\n\n{exitCode, select, 0 {} other {⚠️ Please fix validation errors.}}",
|
|
749
|
+
"toast": {
|
|
750
|
+
"title": "Code Validation",
|
|
751
|
+
"message": "{exitCode, select, 0 {All checks passed ✓} other {Validation failed}}",
|
|
752
|
+
"variant": "{exitCode, select, 0 {success} other {error}}",
|
|
753
|
+
},
|
|
754
|
+
}
|
|
755
|
+
````
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
### Feature Comparison
|
|
760
|
+
|
|
761
|
+
| Feature | Native Plugin | This Plugin |
|
|
762
|
+
| ------------------------ | --------------------------------------- | ---------------------------- |
|
|
763
|
+
| **Setup** | TypeScript, build steps, error handling | JSON/YAML config |
|
|
764
|
+
| **Error Handling** | Manual try/catch required | Automatic, non-blocking |
|
|
765
|
+
| **User Feedback** | Console logs (UI spam) | Toast notifications |
|
|
766
|
+
| **Context Injection** | Manual SDK calls | Automatic |
|
|
767
|
+
| **Tool Filtering** | Basic tool name only | Tool name + ANY arguments |
|
|
768
|
+
| **Subagent Targeting** | Complex parsing required | Native `toolArgs` filter |
|
|
769
|
+
| **Guaranteed Execution** | Depends on agent | Always runs |
|
|
770
|
+
| **Token Cost** | Variable | Zero tokens |
|
|
771
|
+
| **Hot Reload** | Requires rebuild | Edit config, works instantly |
|
|
772
|
+
| **Debugging** | Console.log | OPENCODE_HOOKS_DEBUG=1 |
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
## Known Limitations
|
|
777
|
+
|
|
778
|
+
### Session Hooks Cannot Filter by Agent
|
|
779
|
+
|
|
780
|
+
Session lifecycle events (`session.start`, `session.idle`, `session.end`) don't include the calling agent name. You **cannot** use the `agent` filter field in session hook conditions—it matches all agents.
|
|
781
|
+
|
|
782
|
+
**Workaround:** Use `tool.execute.after` events instead, which provide agent context.
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
## Development
|
|
787
|
+
|
|
788
|
+
```bash
|
|
789
|
+
bun install
|
|
790
|
+
bun run build
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
TODO:
|
|
794
|
+
|
|
795
|
+
- Implement max-length output using tail
|
|
796
|
+
- Add more template functions (date formatting, etc.)
|