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.
Files changed (46) hide show
  1. package/README.md +796 -0
  2. package/dist/config/agent.d.ts +82 -0
  3. package/dist/config/agent.d.ts.map +1 -0
  4. package/dist/config/agent.js +145 -0
  5. package/dist/config/agent.js.map +1 -0
  6. package/dist/config/global.d.ts +36 -0
  7. package/dist/config/global.d.ts.map +1 -0
  8. package/dist/config/global.js +219 -0
  9. package/dist/config/global.js.map +1 -0
  10. package/dist/config/markdown.d.ts +119 -0
  11. package/dist/config/markdown.d.ts.map +1 -0
  12. package/dist/config/markdown.js +373 -0
  13. package/dist/config/markdown.js.map +1 -0
  14. package/dist/config/merge.d.ts +67 -0
  15. package/dist/config/merge.d.ts.map +1 -0
  16. package/dist/config/merge.js +192 -0
  17. package/dist/config/merge.js.map +1 -0
  18. package/dist/execution/shell.d.ts +55 -0
  19. package/dist/execution/shell.d.ts.map +1 -0
  20. package/dist/execution/shell.js +187 -0
  21. package/dist/execution/shell.js.map +1 -0
  22. package/dist/execution/template.d.ts +55 -0
  23. package/dist/execution/template.d.ts.map +1 -0
  24. package/dist/execution/template.js +106 -0
  25. package/dist/execution/template.js.map +1 -0
  26. package/dist/executor.d.ts +54 -0
  27. package/dist/executor.d.ts.map +1 -0
  28. package/dist/executor.js +314 -0
  29. package/dist/executor.js.map +1 -0
  30. package/dist/index.d.ts +22 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +359 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/logging.d.ts +24 -0
  35. package/dist/logging.d.ts.map +1 -0
  36. package/dist/logging.js +57 -0
  37. package/dist/logging.js.map +1 -0
  38. package/dist/schemas.d.ts +425 -0
  39. package/dist/schemas.d.ts.map +1 -0
  40. package/dist/schemas.js +150 -0
  41. package/dist/schemas.js.map +1 -0
  42. package/dist/types/hooks.d.ts +635 -0
  43. package/dist/types/hooks.d.ts.map +1 -0
  44. package/dist/types/hooks.js +12 -0
  45. package/dist/types/hooks.js.map +1 -0
  46. 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.)