@expnn/opencode-yaml-hooks 1.0.1

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 ADDED
@@ -0,0 +1,352 @@
1
+ # opencode-yaml-hooks
2
+
3
+ `opencode-yaml-hooks` is an OpenCode plugin that loads hook definitions from `hooks.yaml` and runs command, tool, or bash actions on session and tool lifecycle events.
4
+
5
+ Use it to run tests after edits, lint changed files, block risky commands before they run, or trigger local automation without another LLM step.
6
+
7
+ ## Install
8
+
9
+ Register it in your `opencode.json`:
10
+
11
+ ```json
12
+ {
13
+ "$schema": "https://opencode.ai/config.json",
14
+ "plugin": ["opencode-yaml-hooks"]
15
+ }
16
+ ```
17
+
18
+ ## Quick start
19
+
20
+ Create one of:
21
+
22
+ - `~/.config/opencode/hook/hooks.yaml`
23
+ - `<project>/.opencode/hook/hooks.yaml`
24
+
25
+ Then add a minimal hook:
26
+
27
+ ```yaml
28
+ hooks:
29
+ - name: npm-test
30
+ event: file.changed
31
+ conditions: [matchesCodeFiles]
32
+ actions:
33
+ - bash: "npm test"
34
+ ```
35
+
36
+ This runs `npm test` after a supported file mutation tool changes at least one tracked code file.
37
+
38
+ ## Current config locations
39
+
40
+ Hooks are merged from global and project locations.
41
+
42
+ | Platform | Global config | Project config |
43
+ |---|---|---|
44
+ | macOS / Linux | `~/.config/opencode/hook/hooks.yaml` | `<project>/.opencode/hook/hooks.yaml` |
45
+ | Windows | `~/.config/opencode/hook/hooks.yaml` preferred, otherwise `%APPDATA%/opencode/hook/hooks.yaml` | `<project>/.opencode/hook/hooks.yaml` |
46
+
47
+ ## Start with these defaults
48
+
49
+ Unless you need something more specific:
50
+
51
+ - prefer `file.changed` for file-oriented automation
52
+ - leave `scope` unset unless you need `main` or `child`
53
+ - leave `runIn` unset unless you need actions to execute in the root session
54
+ - treat `tool.after.*` and `tool.after.<name>` as advanced hooks for observability or non-file workflows
55
+
56
+ Explicit defaults in the current runtime:
57
+
58
+ - `scope` defaults to `all`
59
+ - `runIn` defaults to `current`
60
+ - `conditions` are optional
61
+ - bash `timeout` defaults to `60000` milliseconds
62
+
63
+ ## Schema overview
64
+
65
+ ```yaml
66
+ hooks:
67
+ - name: <hook-name>
68
+ event: <hook-event>
69
+ action: <stop> # optional, only for tool.before.* hooks
70
+ scope: <all|main|child> # optional, defaults to all
71
+ runIn: <current|main> # optional, defaults to current
72
+ async: <boolean> # optional, fire-and-forget execution
73
+ conditions: # optional
74
+ - matchesCodeFiles
75
+ - matchesAnyPath: src/**/*.ts
76
+ - matchesAllPaths:
77
+ - package.json
78
+ - apps/*/package.json
79
+ actions: # required, non-empty
80
+ - command: <string>
81
+ - command:
82
+ name: <string>
83
+ args: <string>
84
+ - tool:
85
+ name: <string>
86
+ args: <object>
87
+ - bash: <string>
88
+ - bash:
89
+ command: <string>
90
+ timeout: <positive integer milliseconds>
91
+ ```
92
+
93
+ Validation rules enforced by the runtime:
94
+
95
+ - `hooks` must exist and be an array
96
+ - each hook must be an object with a supported `event`
97
+ - `scope`, if present, must be `all`, `main`, or `child`
98
+ - `runIn`, if present, must be `current` or `main`
99
+ - `action`, if present, must be `stop` and is only supported on `tool.before.*` and `tool.before.<name>` hooks
100
+ - `async`, if present, must be a boolean; cannot be `true` on `tool.before` or `session.idle` events; async hooks must use only `bash` actions
101
+ - `conditions`, if present, must be an array of supported condition entries
102
+ - `actions` must be a non-empty array
103
+ - each action must define exactly one of `command`, `tool`, or `bash`
104
+
105
+ ## Supported events
106
+
107
+ ### Session events
108
+
109
+ | Event | When it fires |
110
+ |---|---|
111
+ | `session.created` | When OpenCode creates a session |
112
+ | `session.deleted` | When OpenCode deletes a session |
113
+ | `session.idle` | When a session becomes idle |
114
+ | `file.changed` | After a supported mutation tool reports file changes |
115
+
116
+ ### Tool events
117
+
118
+ | Event | When it fires |
119
+ |---|---|
120
+ | `tool.before.*` | Before every tool execution |
121
+ | `tool.before.<name>` | Before a specific tool, such as `tool.before.write` |
122
+ | `tool.after.*` | Advanced: after every tool execution |
123
+ | `tool.after.<name>` | Advanced: after a specific tool, such as `tool.after.edit` |
124
+
125
+ Tool hook order for a tool named `write`:
126
+
127
+ 1. `tool.before.*`
128
+ 2. `tool.before.write`
129
+ 3. tool executes
130
+ 4. `file.changed` if the tool changed tracked files
131
+ 5. `tool.after.*`
132
+ 6. `tool.after.write`
133
+
134
+ ## Public API versus advanced hooks
135
+
136
+ ### Preferred public API: `file.changed`
137
+
138
+ Use `file.changed` when your automation depends on changed files.
139
+
140
+ Why it is preferred:
141
+
142
+ - it only fires for supported mutation tools: `write`, `edit`, `multiedit`, `patch`, and `apply_patch`
143
+ - it includes `files` and structured `changes` metadata
144
+ - it avoids catch-all after-hook ambiguity
145
+ - it is the recommended path for linting, formatting, indexing, and atomic commit workflows
146
+
147
+ ### Advanced escape hatches: `tool.after.*` and `tool.after.<name>`
148
+
149
+ Keep using low-level tool hooks only when you need:
150
+
151
+ - observability for every tool call, including non-file tools
152
+ - tool-specific post-processing unrelated to changed files
153
+ - compatibility with workflows that truly depend on raw tool arguments instead of normalized file changes
154
+
155
+ ## Conditions
156
+
157
+ All configured conditions must pass for a hook to run.
158
+
159
+ | Condition | Meaning |
160
+ |---|---|
161
+ | `matchesCodeFiles` | Run only when tracked modified files include at least one supported code extension |
162
+ | `matchesAnyPath` | Run only when at least one final changed file path matches one or more glob patterns |
163
+ | `matchesAllPaths` | Run only when every final changed file path matches at least one glob pattern |
164
+
165
+ `matchesCodeFiles` is extension-based. Extensionless files such as `Dockerfile` do not currently count as code changes.
166
+
167
+ `matchesAnyPath` and `matchesAllPaths` only work on `file.changed` and `session.idle`. Both accept either a single string or a string array, and both fail when there are no changed files to evaluate.
168
+
169
+ Examples:
170
+
171
+ ```yaml
172
+ hooks:
173
+ - event: file.changed
174
+ conditions:
175
+ - matchesAnyPath: src/**/*.ts
176
+ actions:
177
+ - bash: "npm run lint -- --fix"
178
+
179
+ - event: session.idle
180
+ scope: main
181
+ conditions:
182
+ - matchesAllPaths:
183
+ - package.json
184
+ - apps/*/package.json
185
+ actions:
186
+ - bash: "npm test"
187
+ ```
188
+
189
+ Invalid usage example:
190
+
191
+ ```yaml
192
+ hooks:
193
+ - event: tool.after.write
194
+ conditions:
195
+ - matchesAnyPath: src/**/*.ts # invalid: path conditions are file.changed/session.idle only
196
+ actions:
197
+ - bash: "echo nope"
198
+ ```
199
+
200
+ ## Actions
201
+
202
+ ### Command action
203
+
204
+ Runs an OpenCode command in the same session, unless `runIn: main` redirects it to the root session.
205
+
206
+ ```yaml
207
+ actions:
208
+ - command: simplify-changes
209
+ - command:
210
+ name: review-pr
211
+ args: "main feature"
212
+ ```
213
+
214
+ ### Tool action
215
+
216
+ Prompts the session to use a tool with specific arguments.
217
+
218
+ ```yaml
219
+ actions:
220
+ - tool:
221
+ name: bash
222
+ args:
223
+ command: "echo done"
224
+ ```
225
+
226
+ ### Bash action
227
+
228
+ Runs a bash command directly without another LLM step.
229
+
230
+ ```yaml
231
+ actions:
232
+ - bash: "npm run lint"
233
+ - bash:
234
+ command: "$OPENCODE_PROJECT_DIR/.opencode/hooks/init.sh"
235
+ timeout: 30000
236
+ ```
237
+
238
+ If `timeout` is omitted, bash actions use the runtime default of `60000` milliseconds.
239
+
240
+ `OPENCODE_PROJECT_DIR` remains the action cwd / project directory that triggered the hook.
241
+
242
+ ## Bash payloads
243
+
244
+ Every bash action receives:
245
+
246
+ - inherited `process.env`
247
+ - `OPENCODE_PROJECT_DIR` for the action cwd / project directory
248
+ - `OPENCODE_SESSION_ID`
249
+ - `OPENCODE_GIT_COMMON_DIR` when available
250
+ - JSON over stdin
251
+
252
+ Example `file.changed` payload:
253
+
254
+ ```json
255
+ {
256
+ "session_id": "abc123",
257
+ "event": "file.changed",
258
+ "cwd": "/path/to/project",
259
+ "files": ["src/index.ts", "src/renamed.ts"],
260
+ "changes": [
261
+ { "operation": "modify", "path": "src/index.ts" },
262
+ { "operation": "rename", "fromPath": "src/old.ts", "toPath": "src/renamed.ts" }
263
+ ],
264
+ "tool_name": "apply_patch",
265
+ "tool_args": {
266
+ "patchText": "*** Begin Patch\\n...\\n*** End Patch"
267
+ }
268
+ }
269
+ ```
270
+
271
+ ## Blocking behavior
272
+
273
+ Only `tool.before.*` and `tool.before.<name>` hooks can block execution.
274
+
275
+ - a bash action that exits with `2` blocks the tool
276
+ - `action: stop` escalates a blocking pre-tool hook into a best-effort `session.abort(...)` for the active session
277
+ - `tool.after.*`, `tool.after.<name>`, `file.changed`, and session hooks do not block execution
278
+ - non-blocking failures are logged and later actions continue
279
+
280
+ Example:
281
+
282
+ ```yaml
283
+ hooks:
284
+ - event: tool.before.bash
285
+ action: stop
286
+ actions:
287
+ - bash: |
288
+ payload=$(cat)
289
+ cmd=$(printf '%s' "$payload" | jq -r '.tool_args.command // empty')
290
+
291
+ case "$cmd" in
292
+ "git push"|git\ push\ *)
293
+ echo "Blocked and stopping session: git push is not allowed." >&2
294
+ exit 2
295
+ ;;
296
+ esac
297
+ ```
298
+
299
+ ## Execution behavior on this branch
300
+
301
+ - hooks for the same event run in declaration order
302
+ - global hooks load before project hooks
303
+ - the runtime reloads discovered `hooks.yaml` files at each hook entrypoint
304
+ - invalid reloads are rejected and the last known good config stays active
305
+ - `session.idle` clears tracked changes only after successful dispatch
306
+ - if idle dispatch fails, tracked changes are preserved for retry
307
+ - reentrant `file.changed` and `tool.after.*` dispatches are queued and replayed after the active dispatch finishes
308
+ - `async: true` hooks return immediately without blocking the tool pipeline; their actions run in the background as best-effort work
309
+ - async actions for the same event and source session are serialized to prevent overlapping executions; note that serialization is per source session, not per target — `runIn: main` hooks from different child sessions are not serialized against each other
310
+
311
+ ## Copy-paste examples
312
+
313
+ See [`examples/hooks.yaml`](examples/hooks.yaml) for:
314
+
315
+ - main-session only examples
316
+ - child-to-main `runIn: main` routing
317
+ - recommended `file.changed` automation
318
+ - advanced `tool.after.*` observability
319
+ - conservative atomic commit wiring
320
+
321
+ ## See also
322
+
323
+ - [`docs/hooks-v2-reference.md`](docs/hooks-v2-reference.md) for the current public config shape
324
+ - [`docs/comparison-with-claude-code-hooks.md`](docs/comparison-with-claude-code-hooks.md) for how this compares to Claude Code's hook system
325
+
326
+ ## Known limitations
327
+
328
+ - file tracking is limited to supported OpenCode mutation tools, not arbitrary filesystem changes
329
+ - `matchesCodeFiles` is extension-based and ignores extensionless code-like files
330
+ - tool hooks depend on actual emitted OpenCode tool names
331
+ - Windows discovery is supported, but bash actions still require a working shell runtime
332
+ - `async: true` is not allowed on `tool.before.*` or `session.idle` events; async hooks cannot block tool execution or idle dispatch
333
+ - async hooks must use only `bash` actions; `command` and `tool` actions have no timeout and can stall the queue
334
+ - async hook failures are logged but not retried; async execution is best-effort and not guaranteed to complete if the host process exits
335
+
336
+ ## Explicit non-goals for v1/v2 runtime scope
337
+
338
+ This package does **not** currently try to:
339
+
340
+ - define custom hook events beyond session, file, and tool lifecycle events
341
+ - provide config inheritance or override priority beyond global-then-project merging
342
+ - provide retries, scheduling, or concurrency controls per hook
343
+ - track arbitrary filesystem changes outside OpenCode mutation tools
344
+ - make command or tool actions blocking
345
+
346
+ ## Development
347
+
348
+ ```bash
349
+ npm install
350
+ npm run build
351
+ npm test
352
+ ```
@@ -0,0 +1,2 @@
1
+ import type { Hooks, PluginInput } from "@opencode-ai/plugin";
2
+ export declare function createOpencodeHooksPlugin(input: PluginInput): Promise<Hooks>;
@@ -0,0 +1,4 @@
1
+ import { type BashExecutionRequest, type BashHookContext, type BashHookResult, type BashProcessResult } from "./bash-types.js";
2
+ export declare function executeBashHook(request: BashExecutionRequest): Promise<BashHookResult>;
3
+ export declare function mapBashProcessResultToHookResult(result: BashProcessResult, context: BashHookContext): BashHookResult;
4
+ export declare function isBlockingToolBeforeEvent(event: string): boolean;
@@ -0,0 +1,33 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ import type { FileChange } from "./types.js";
3
+ export declare const DEFAULT_BASH_TIMEOUT = 60000;
4
+ export interface BashHookContext {
5
+ readonly session_id: string;
6
+ readonly event: string;
7
+ readonly cwd: string;
8
+ readonly files?: readonly string[];
9
+ readonly changes?: readonly FileChange[];
10
+ readonly tool_name?: string;
11
+ readonly tool_args?: Record<string, unknown>;
12
+ }
13
+ export interface BashExecutionRequest {
14
+ readonly command: string;
15
+ readonly context: BashHookContext;
16
+ readonly projectDir: string;
17
+ readonly timeout?: number;
18
+ readonly client?: PluginInput["client"];
19
+ }
20
+ export interface BashProcessResult {
21
+ readonly command: string;
22
+ readonly stdout: string;
23
+ readonly stderr: string;
24
+ readonly durationMs: number;
25
+ readonly exitCode: number;
26
+ readonly signal: NodeJS.Signals | null;
27
+ readonly timedOut: boolean;
28
+ }
29
+ export type BashHookResultStatus = "success" | "failed" | "blocked" | "timed_out";
30
+ export interface BashHookResult extends BashProcessResult {
31
+ readonly status: BashHookResultStatus;
32
+ readonly blocking: boolean;
33
+ }
@@ -0,0 +1,13 @@
1
+ export interface HookConfigDiscoveryOptions {
2
+ readonly projectDir?: string;
3
+ readonly platform?: string;
4
+ readonly homeDir?: string;
5
+ readonly appDataDir?: string;
6
+ readonly exists?: (filePath: string) => boolean;
7
+ }
8
+ export interface HookConfigPaths {
9
+ readonly global?: string;
10
+ readonly project?: string;
11
+ }
12
+ export declare function resolveHookConfigPaths(options?: HookConfigDiscoveryOptions): HookConfigPaths;
13
+ export declare function discoverHookConfigPaths(options?: HookConfigDiscoveryOptions): string[];
@@ -0,0 +1,26 @@
1
+ import { type HookOverrideEntry, type HookMap, type HookValidationError, type ParsedHooksFile } from "./types.js";
2
+ import { type HookConfigDiscoveryOptions } from "./config-paths.js";
3
+ export interface HookDiscoveryResult {
4
+ readonly hooks: HookMap;
5
+ readonly errors: HookValidationError[];
6
+ readonly files: string[];
7
+ }
8
+ export interface HookLoadOptions extends HookConfigDiscoveryOptions {
9
+ readonly readFile?: (filePath: string) => string;
10
+ }
11
+ export interface HookLoadSnapshot extends HookDiscoveryResult {
12
+ readonly signature: string;
13
+ }
14
+ type ParsedHooksFileResult = ParsedHooksFile & {
15
+ readonly files: string[];
16
+ };
17
+ export declare function parseHooksFile(filePath: string, content: string): ParsedHooksFileResult;
18
+ export declare function loadHooksFile(filePath: string, readFile?: (filePath: string) => string): ParsedHooksFileResult;
19
+ export declare function loadDiscoveredHooks(options?: HookLoadOptions): HookDiscoveryResult;
20
+ export declare function loadDiscoveredHooksSnapshot(options?: HookLoadOptions): HookLoadSnapshot;
21
+ export declare function mergeHookMaps(...hookMaps: HookMap[]): HookMap;
22
+ export declare function resolveOverrides(hooks: HookMap, overrides: HookOverrideEntry[]): {
23
+ hooks: HookMap;
24
+ errors: HookValidationError[];
25
+ };
26
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { Hooks, PluginInput } from "@opencode-ai/plugin";
2
+ import { executeBashHook } from "./bash-executor.js";
3
+ import type { BashExecutionRequest } from "./bash-types.js";
4
+ import type { HookMap } from "./types.js";
5
+ type ExecuteBashHook = (request: BashExecutionRequest) => ReturnType<typeof executeBashHook>;
6
+ export interface CreateHooksRuntimeOptions {
7
+ readonly hooks?: HookMap;
8
+ readonly executeBash?: ExecuteBashHook;
9
+ readonly client?: PluginInput["client"];
10
+ }
11
+ export declare function createHooksRuntime(input: PluginInput, options?: CreateHooksRuntimeOptions): Hooks;
12
+ export {};
@@ -0,0 +1,25 @@
1
+ import type { FileChange } from "./types.js";
2
+ export interface PendingToolCall {
3
+ readonly sessionID: string;
4
+ readonly toolArgs: Record<string, unknown>;
5
+ }
6
+ export type SessionScope = "all" | "main" | "child";
7
+ export declare class SessionStateStore {
8
+ private readonly sessions;
9
+ private readonly pendingToolCalls;
10
+ rememberSession(sessionID: string, parentID?: string | null): void;
11
+ evaluateScope(sessionID: string, scope: SessionScope, resolveParentID: (sessionID: string) => Promise<string | null | undefined>): Promise<boolean>;
12
+ getRootSessionID(sessionID: string, resolveParentID: (sessionID: string) => Promise<string | null | undefined>): Promise<string>;
13
+ isDeleted(sessionID: string): boolean;
14
+ deleteSession(sessionID: string): void;
15
+ setPendingToolCall(callID: string, sessionID: string, toolArgs: Record<string, unknown>): void;
16
+ consumePendingToolCall(callID: string): PendingToolCall | undefined;
17
+ addFileChanges(sessionID: string, changes: Iterable<FileChange>): void;
18
+ getFileChanges(sessionID: string): FileChange[];
19
+ getModifiedPaths(sessionID: string): string[];
20
+ beginIdleDispatch(sessionID: string, changes: readonly FileChange[]): void;
21
+ consumeFileChanges(sessionID: string, changes: readonly FileChange[]): void;
22
+ cancelIdleDispatch(sessionID: string): void;
23
+ private getOrCreateSession;
24
+ private resolveRootSessionID;
25
+ }
@@ -0,0 +1,13 @@
1
+ import type { FileChange } from "./types.js";
2
+ declare const DIRECT_MUTATION_TOOL_NAMES: readonly ["write", "edit", "multiedit"];
3
+ declare const PATCH_MUTATION_TOOL_NAMES: readonly ["patch", "apply_patch"];
4
+ declare const BASH_TOOL_NAME: "bash";
5
+ export declare const MUTATION_TOOL_NAMES: Set<"bash" | "write" | "edit" | "multiedit" | "patch" | "apply_patch">;
6
+ export type MutationToolName = (typeof DIRECT_MUTATION_TOOL_NAMES)[number] | (typeof PATCH_MUTATION_TOOL_NAMES)[number] | typeof BASH_TOOL_NAME;
7
+ export type NormalizedMutationToolName = "write" | "edit" | "multiedit" | "apply_patch" | "bash";
8
+ export declare function normalizeMutationToolName(toolName: string): NormalizedMutationToolName | undefined;
9
+ export declare function getMutationToolHookNames(toolName: string): string[];
10
+ export declare function getToolAffectedPaths(toolName: string, args: Record<string, unknown>): string[];
11
+ export declare function getToolFileChanges(toolName: string, args: Record<string, unknown>): FileChange[];
12
+ export declare function getChangedPaths(changes: readonly FileChange[]): string[];
13
+ export {};
@@ -0,0 +1,102 @@
1
+ export declare const SESSION_HOOK_EVENTS: readonly ["session.idle", "session.created", "session.deleted", "file.changed"];
2
+ export declare const LEGACY_HOOK_CONDITIONS: readonly ["matchesCodeFiles"];
3
+ export declare const PATH_HOOK_CONDITION_KEYS: readonly ["matchesAnyPath", "matchesAllPaths"];
4
+ export declare const HOOK_SCOPES: readonly ["all", "main", "child"];
5
+ export declare const HOOK_RUN_IN: readonly ["current", "main"];
6
+ export declare const HOOK_BEHAVIORS: readonly ["stop"];
7
+ export type SessionHookEvent = (typeof SESSION_HOOK_EVENTS)[number];
8
+ export type ToolHookPhase = "before" | "after";
9
+ export type ToolHookEvent = `tool.${ToolHookPhase}.*` | `tool.${ToolHookPhase}.${string}`;
10
+ export type HookEvent = SessionHookEvent | ToolHookEvent;
11
+ export type HookLegacyCondition = (typeof LEGACY_HOOK_CONDITIONS)[number];
12
+ export type HookPathConditionKey = (typeof PATH_HOOK_CONDITION_KEYS)[number];
13
+ export type HookPathCondition = {
14
+ readonly matchesAnyPath: readonly string[];
15
+ } | {
16
+ readonly matchesAllPaths: readonly string[];
17
+ };
18
+ export type HookCondition = HookLegacyCondition | HookPathCondition;
19
+ export type HookScope = (typeof HOOK_SCOPES)[number];
20
+ export type HookRunIn = (typeof HOOK_RUN_IN)[number];
21
+ export type HookBehavior = (typeof HOOK_BEHAVIORS)[number];
22
+ export interface CreateFileChange {
23
+ readonly operation: "create";
24
+ readonly path: string;
25
+ }
26
+ export interface ModifyFileChange {
27
+ readonly operation: "modify";
28
+ readonly path: string;
29
+ }
30
+ export interface DeleteFileChange {
31
+ readonly operation: "delete";
32
+ readonly path: string;
33
+ }
34
+ export interface RenameFileChange {
35
+ readonly operation: "rename";
36
+ readonly fromPath: string;
37
+ readonly toPath: string;
38
+ }
39
+ export type FileChange = CreateFileChange | ModifyFileChange | DeleteFileChange | RenameFileChange;
40
+ export interface HookCommandActionConfig {
41
+ readonly name: string;
42
+ readonly args?: string;
43
+ }
44
+ export interface HookToolActionConfig {
45
+ readonly name: string;
46
+ readonly args?: Record<string, unknown>;
47
+ }
48
+ export interface HookBashActionConfig {
49
+ readonly command: string;
50
+ readonly timeout?: number;
51
+ }
52
+ export interface HookCommandAction {
53
+ readonly command: string | HookCommandActionConfig;
54
+ }
55
+ export interface HookToolAction {
56
+ readonly tool: HookToolActionConfig;
57
+ }
58
+ export interface HookBashAction {
59
+ readonly bash: string | HookBashActionConfig;
60
+ }
61
+ export type HookAction = HookCommandAction | HookToolAction | HookBashAction;
62
+ export interface HookConfigSource {
63
+ readonly filePath: string;
64
+ readonly index: number;
65
+ }
66
+ export interface HookConfig {
67
+ readonly id?: string;
68
+ readonly event: HookEvent;
69
+ readonly name?: string;
70
+ readonly action?: HookBehavior;
71
+ readonly actions: HookAction[];
72
+ readonly scope: HookScope;
73
+ readonly runIn: HookRunIn;
74
+ readonly async?: boolean;
75
+ readonly conditions?: HookCondition[];
76
+ readonly source: HookConfigSource;
77
+ }
78
+ export interface HookOverrideEntry {
79
+ readonly targetId: string;
80
+ readonly disable: boolean;
81
+ readonly replacement?: HookConfig;
82
+ readonly source: HookConfigSource;
83
+ }
84
+ export type HookMap = Map<HookEvent, HookConfig[]>;
85
+ export type HookValidationErrorCode = "invalid_frontmatter" | "missing_hooks" | "invalid_hooks" | "invalid_hook" | "invalid_event" | "invalid_scope" | "invalid_run_in" | "invalid_hook_action" | "invalid_conditions" | "invalid_actions" | "invalid_action" | "duplicate_hook_id" | "override_target_not_found" | "invalid_override" | "invalid_async";
86
+ export interface HookValidationError {
87
+ readonly code: HookValidationErrorCode;
88
+ readonly filePath: string;
89
+ readonly message: string;
90
+ readonly path?: string;
91
+ }
92
+ export interface ParsedHooksFile {
93
+ readonly hooks: HookMap;
94
+ readonly overrides: HookOverrideEntry[];
95
+ readonly errors: HookValidationError[];
96
+ }
97
+ export declare function isHookEvent(value: unknown): value is HookEvent;
98
+ export declare function isHookLegacyCondition(value: unknown): value is HookLegacyCondition;
99
+ export declare function isHookPathConditionKey(value: unknown): value is HookPathConditionKey;
100
+ export declare function isHookScope(value: unknown): value is HookScope;
101
+ export declare function isHookRunIn(value: unknown): value is HookRunIn;
102
+ export declare function isHookBehavior(value: unknown): value is HookBehavior;
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ declare const OpencodeHooksPlugin: Plugin;
3
+ export default OpencodeHooksPlugin;