@mauribadnights/clooks 0.5.0 → 0.5.2

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.
@@ -0,0 +1,42 @@
1
+ # Async Handlers
2
+
3
+ ## Overview
4
+
5
+ Set `async: true` on any handler to execute it without blocking Claude Code's response. The handler runs in the background; its output is NOT included in the hook response.
6
+
7
+ ## Use Cases
8
+
9
+ - Logging and analytics
10
+ - Session tracking
11
+ - Background notifications
12
+ - Non-critical metric collection
13
+
14
+ ## Configuration
15
+
16
+ ```yaml
17
+ handlers:
18
+ UserPromptSubmit:
19
+ - id: prompt-analytics
20
+ type: inline
21
+ module: ~/hooks/analytics.js
22
+ async: true
23
+ ```
24
+
25
+ ## Behavior
26
+
27
+ - Fires immediately, does not await completion
28
+ - Errors are swallowed (logged to `daemon.log` but don't affect response)
29
+ - Results delivered via internal `onAsyncResult` callback
30
+ - Metrics still recorded for async handlers
31
+
32
+ ## Limitations
33
+
34
+ - Output NOT included in hook response to Claude Code
35
+ - If `depends` is set on an async handler, it is forced synchronous (with warning)
36
+ - Cannot be depended upon by other handlers
37
+
38
+ > **Note:** Async handlers are ideal for side effects that should never slow down the user experience. If you need the handler's output to influence Claude's behavior, use a synchronous handler instead.
39
+
40
+ ---
41
+
42
+ [Home](../index.md) | [Prev: Dependencies](dependencies.md) | [Next: Short-Circuit](short-circuit.md)
@@ -0,0 +1,153 @@
1
+ # Dependencies
2
+
3
+ Handlers can declare dependencies on other handlers using the `depends` field. clooks resolves dependencies into execution "waves" using topological sort (Kahn's algorithm), running independent handlers in parallel while respecting ordering constraints.
4
+
5
+ ## Overview
6
+
7
+ Without dependencies, all handlers for an event run in parallel. With dependencies, handlers are grouped into sequential waves:
8
+
9
+ - **Wave 0:** Handlers with no dependencies.
10
+ - **Wave 1:** Handlers whose dependencies are all in wave 0.
11
+ - **Wave N:** Handlers whose dependencies are all in waves 0 through N-1.
12
+
13
+ Handlers within the same wave run in parallel. Waves execute sequentially.
14
+
15
+ ## How It Works
16
+
17
+ 1. The dependency graph is built from `depends` fields across all eligible handlers for the event.
18
+ 2. Handlers are sorted into waves using Kahn's algorithm (BFS topological sort).
19
+ 3. Wave 0 executes first. All handlers in wave 0 run in parallel.
20
+ 4. When wave 0 completes, wave 1 starts. Its handlers can access outputs from wave 0.
21
+ 5. This continues until all waves have executed.
22
+
23
+ ## Example
24
+
25
+ ```yaml
26
+ handlers:
27
+ PreToolUse:
28
+ - id: context-loader
29
+ type: inline
30
+ module: ~/hooks/context.js
31
+ # No depends -- Wave 0
32
+
33
+ - id: security-check
34
+ type: llm
35
+ model: claude-haiku-4-5
36
+ prompt: "Check security of $TOOL_NAME with args: $ARGUMENTS"
37
+ # No depends -- Wave 0 (parallel with context-loader)
38
+
39
+ - id: deep-review
40
+ type: llm
41
+ model: claude-sonnet-4-6
42
+ prompt: "Perform deep review with full context: $ARGUMENTS"
43
+ depends: [context-loader, security-check]
44
+ # Both deps in Wave 0 -- this runs in Wave 1
45
+ ```
46
+
47
+ Execution order:
48
+
49
+ ```
50
+ Wave 0: context-loader + security-check (parallel)
51
+ |
52
+ v
53
+ Wave 1: deep-review (after both wave 0 handlers complete)
54
+ ```
55
+
56
+ ## Accessing Dependency Outputs
57
+
58
+ Handlers in wave N receive outputs from all previous waves via the `_handlerOutputs` field injected into their input:
59
+
60
+ ```json
61
+ {
62
+ "session_id": "...",
63
+ "cwd": "...",
64
+ "hook_event_name": "PreToolUse",
65
+ "tool_name": "Write",
66
+ "_handlerOutputs": {
67
+ "context-loader": {
68
+ "additionalContext": "Loaded project context..."
69
+ },
70
+ "security-check": {
71
+ "additionalContext": "No security issues found."
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ For inline handlers, access it directly from the input object:
78
+
79
+ ```javascript
80
+ export default async function(input) {
81
+ const priorResults = input._handlerOutputs || {};
82
+ const securityResult = priorResults['security-check'];
83
+
84
+ // Use prior results to inform this handler's logic
85
+ if (securityResult?.additionalContext?.includes('issue')) {
86
+ return { decision: 'block', reason: 'Security issue detected upstream' };
87
+ }
88
+
89
+ return { additionalContext: 'All clear after deep review.' };
90
+ }
91
+ ```
92
+
93
+ For LLM handlers, dependency outputs are available in the input but not directly interpolable into prompt templates. Use an inline handler as a dependency to prepare context that downstream LLM handlers can consume.
94
+
95
+ ## Cycle Detection
96
+
97
+ Circular dependencies are detected at execution time. If a cycle exists, clooks throws an error identifying the affected handler IDs:
98
+
99
+ ```
100
+ Error: Dependency cycle detected among handlers: handler-a, handler-b
101
+ ```
102
+
103
+ The daemon logs the error and skips all handlers involved in the cycle. Other handlers in the same event that are not part of the cycle execute normally.
104
+
105
+ ## Cross-Plugin Dependencies
106
+
107
+ Plugin handlers are namespaced as `pluginName/handlerId`. Dependency references follow these rules:
108
+
109
+ | Reference Style | Resolves To |
110
+ |-----------------|-------------|
111
+ | `depends: [other-handler]` | Same-plugin handler (auto-namespaced) |
112
+ | `depends: [other-plugin/handler-id]` | Handler from a different plugin |
113
+ | `depends: [user-handler-id]` | Handler defined in the user manifest |
114
+
115
+ Example with a plugin handler depending on a user-defined handler:
116
+
117
+ ```yaml
118
+ # In clooks-plugin.yaml (plugin: my-plugin)
119
+ handlers:
120
+ PreToolUse:
121
+ - id: plugin-review
122
+ type: llm
123
+ model: claude-haiku-4-5
124
+ prompt: "Review after context load: $ARGUMENTS"
125
+ depends: [context-loader] # References user manifest handler
126
+ ```
127
+
128
+ The fully qualified ID of this handler is `my-plugin/plugin-review`. Other plugins or user handlers can depend on it using that full name.
129
+
130
+ ## Async and Dependencies
131
+
132
+ Async handlers (`async: true`) that participate in dependency relationships are forced to run synchronously. This applies when:
133
+
134
+ - An async handler has `depends` referencing other handlers in the same event.
135
+ - Other handlers in the same event list an async handler in their `depends`.
136
+
137
+ In both cases, clooks logs a warning and runs the handler synchronously:
138
+
139
+ ```
140
+ [clooks] Warning: async handler "my-handler" has dependency relationships, running synchronously
141
+ ```
142
+
143
+ This is because fire-and-forget execution cannot guarantee dependency ordering. If you need a handler to be truly async, remove it from all dependency chains.
144
+
145
+ ## Dependencies and Filtering
146
+
147
+ Dependencies are resolved after filtering. If a handler's dependency is filtered out (by keyword filter, agent, or project scope), the dependency is treated as satisfied. The dependent handler will not find that dependency's output in `_handlerOutputs`, but it will not be blocked waiting for it.
148
+
149
+ Only dependencies referencing handlers within the current event's eligible set are considered. References to unknown handler IDs are silently ignored.
150
+
151
+ ---
152
+
153
+ [Home](../index.md) | [Prev: Filtering](filtering.md) | [Next: Async Handlers](async-handlers.md)
@@ -0,0 +1,153 @@
1
+ # Filtering
2
+
3
+ Handlers can be scoped to fire only under specific conditions. clooks provides three filtering mechanisms: keyword filters, agent scoping, and project scoping. All filters are AND'd together -- a handler only fires if every applicable filter passes.
4
+
5
+ ## Keyword Filters
6
+
7
+ The `filter` field applies a keyword match against the JSON-stringified hook input. Matching is case-insensitive.
8
+
9
+ ### Syntax
10
+
11
+ | Pattern | Meaning |
12
+ |---------|---------|
13
+ | `"word1\|word2"` | Match if ANY keyword is found (OR) |
14
+ | `"!word"` | Exclude if keyword is found (NOT) |
15
+ | `"word1\|!word2"` | Match if word1 is present AND word2 is absent |
16
+
17
+ The filter string is split on `|`. Each term is classified as positive (no prefix) or negative (`!` prefix). The rules are:
18
+
19
+ 1. If ANY negative term is found in the input, the handler is **blocked**.
20
+ 2. If there are positive terms, at least ONE must be found for the handler to **fire**.
21
+ 3. If there are only negative terms and none matched, the handler **fires**.
22
+
23
+ ### Examples
24
+
25
+ **Fire only for Bash or Execute tools:**
26
+
27
+ ```yaml
28
+ - id: bash-guard
29
+ type: script
30
+ command: "node ~/hooks/guard.js"
31
+ filter: "Bash|Execute"
32
+ ```
33
+
34
+ **Fire for everything except Read and Glob:**
35
+
36
+ ```yaml
37
+ - id: write-logger
38
+ type: inline
39
+ module: ~/hooks/logger.js
40
+ filter: "!Read|!Glob"
41
+ ```
42
+
43
+ This works because both `Read` and `Glob` are negative terms. The handler fires whenever neither term appears in the input.
44
+
45
+ **Fire for Write unless "test" appears in the input:**
46
+
47
+ ```yaml
48
+ - id: write-review
49
+ type: llm
50
+ model: claude-haiku-4-5
51
+ prompt: "Review: $ARGUMENTS"
52
+ filter: "Write|!test"
53
+ ```
54
+
55
+ Here `Write` is a positive term and `test` is negative. The handler fires when the input contains "Write" but does not contain "test".
56
+
57
+ > **Note:** The filter matches against the entire JSON-stringified hook input, not just the tool name. This means field values, file paths, and argument content are all searchable.
58
+
59
+ ## Agent Scoping
60
+
61
+ The `agent` field restricts a handler to specific Claude Code agent sessions.
62
+
63
+ ### Syntax
64
+
65
+ A comma-separated list of agent names (case-insensitive). The handler only fires when the current session's agent matches one of the listed names.
66
+
67
+ ```yaml
68
+ - id: builder-guard
69
+ type: script
70
+ command: "node ~/hooks/builder-guard.js"
71
+ agent: "builder"
72
+ ```
73
+
74
+ ```yaml
75
+ - id: multi-agent-hook
76
+ type: inline
77
+ module: ~/hooks/shared.js
78
+ agent: "builder,coo"
79
+ ```
80
+
81
+ ### How Agent Detection Works
82
+
83
+ The agent name is extracted from the `agent_type` field of the `SessionStart` event payload. clooks caches this per `session_id`. Subsequent events in the same session use the cached value.
84
+
85
+ If `agent` is omitted from a handler, it fires in all sessions regardless of agent type.
86
+
87
+ ## Project Scoping
88
+
89
+ The `project` field restricts a handler to sessions running in specific directories. It is matched against the `cwd` field of the hook input.
90
+
91
+ ### Matching Rules
92
+
93
+ - **With wildcards (`*`):** The pattern is split on `*` and each literal segment must appear in the cwd path. Order does not matter.
94
+ - **Without wildcards:** The cwd must start with the pattern (prefix match) or equal it exactly.
95
+
96
+ ### Examples
97
+
98
+ **Only fire in Driffusion projects:**
99
+
100
+ ```yaml
101
+ - id: driffusion-lint
102
+ type: script
103
+ command: "node ~/hooks/driffusion-lint.js"
104
+ project: "*/Driffusion/*"
105
+ ```
106
+
107
+ This matches any cwd containing `/Driffusion/` anywhere in the path.
108
+
109
+ **Only fire in a specific directory:**
110
+
111
+ ```yaml
112
+ - id: work-hook
113
+ type: inline
114
+ module: ~/hooks/work.js
115
+ project: "/Users/me/work"
116
+ ```
117
+
118
+ This matches any cwd that starts with `/Users/me/work`.
119
+
120
+ ## Combining Filters
121
+
122
+ All filters are evaluated in order. A handler fires only if every condition passes:
123
+
124
+ 1. `enabled` is not `false`.
125
+ 2. The handler is not auto-disabled (consecutive failures < 3).
126
+ 3. `agent` matches the current session agent (if specified).
127
+ 4. `project` matches the session cwd (if specified).
128
+ 5. `filter` keyword match passes (if specified).
129
+
130
+ If any condition fails, the handler is skipped. Skipped handlers are recorded in metrics with `filtered: true` and zero execution time.
131
+
132
+ ### Full Example
133
+
134
+ ```yaml
135
+ handlers:
136
+ PreToolUse:
137
+ - id: targeted-review
138
+ type: llm
139
+ model: claude-haiku-4-5
140
+ prompt: "Review: $ARGUMENTS"
141
+ filter: "Write|Edit"
142
+ agent: "builder"
143
+ project: "*/Driffusion/*"
144
+ ```
145
+
146
+ This handler fires only when:
147
+ - The tool call input contains "Write" or "Edit".
148
+ - The session is running the `builder` agent.
149
+ - The working directory contains `/Driffusion/` in its path.
150
+
151
+ ---
152
+
153
+ [Home](../index.md) | [Prev: LLM Handlers](llm-handlers.md) | [Next: Dependencies](dependencies.md)
@@ -0,0 +1,236 @@
1
+ # Handlers
2
+
3
+ clooks supports three handler types: **script**, **inline**, and **llm**. Each type trades off between flexibility and performance. This guide covers how each type works, when to use it, and how to structure the output.
4
+
5
+ ## Script Handlers
6
+
7
+ Script handlers run shell commands via `sh -c`. They are the most portable option -- any language that reads stdin and writes stdout works.
8
+
9
+ ### How They Work
10
+
11
+ 1. clooks spawns a child process with the handler's `command`.
12
+ 2. The full `HookInput` JSON is piped to the process's stdin.
13
+ 3. The process writes its response to stdout.
14
+ 4. If stdout is valid JSON, it is used directly. If not, the raw text is wrapped as `{"additionalContext": "..."}`.
15
+
16
+ ### Required Fields
17
+
18
+ | Field | Type | Description |
19
+ |-------|------|-------------|
20
+ | `command` | string | Shell command to execute via `sh -c` |
21
+
22
+ ### Default Timeout
23
+
24
+ 5000ms. Override with the `timeout` field.
25
+
26
+ ### Example Configuration
27
+
28
+ ```yaml
29
+ handlers:
30
+ PreToolUse:
31
+ - id: bash-guard
32
+ type: script
33
+ command: "node ~/.clooks/hooks/bash-guard.js"
34
+ filter: "Bash"
35
+ timeout: 3000
36
+ ```
37
+
38
+ ### Example Script (Node.js)
39
+
40
+ ```javascript
41
+ #!/usr/bin/env node
42
+
43
+ // Read HookInput from stdin
44
+ let data = '';
45
+ process.stdin.on('data', chunk => { data += chunk; });
46
+ process.stdin.on('end', () => {
47
+ const input = JSON.parse(data);
48
+
49
+ // Check if the Bash command looks dangerous
50
+ const args = input.tool_input || {};
51
+ const command = args.command || '';
52
+
53
+ if (command.includes('rm -rf /')) {
54
+ // Block the tool call
55
+ console.log(JSON.stringify({
56
+ decision: 'block',
57
+ reason: 'Dangerous rm -rf command detected'
58
+ }));
59
+ } else {
60
+ // Add context for Claude
61
+ console.log(JSON.stringify({
62
+ additionalContext: `Bash command reviewed: ${command.slice(0, 80)}`
63
+ }));
64
+ }
65
+ });
66
+ ```
67
+
68
+ > **Note:** Non-zero exit codes are treated as handler failures. Stderr output is captured and included in the error message.
69
+
70
+ ## Inline Handlers
71
+
72
+ Inline handlers import an ES module in-process. There is no subprocess overhead, making them the fastest handler type.
73
+
74
+ ### How They Work
75
+
76
+ 1. clooks dynamically imports the module specified by `module`.
77
+ 2. The module's default export is called with the `HookInput` object.
78
+ 3. The return value becomes the handler output.
79
+
80
+ ### Required Fields
81
+
82
+ | Field | Type | Description |
83
+ |-------|------|-------------|
84
+ | `module` | string | Path to a `.js` or `.ts` file with a default export function |
85
+
86
+ ### Default Timeout
87
+
88
+ 5000ms. Override with the `timeout` field.
89
+
90
+ ### Example Configuration
91
+
92
+ ```yaml
93
+ handlers:
94
+ PreToolUse:
95
+ - id: context-injector
96
+ type: inline
97
+ module: ~/.clooks/hooks/context.js
98
+ filter: "Write|Edit"
99
+ ```
100
+
101
+ ### Example Module
102
+
103
+ ```typescript
104
+ // ~/.clooks/hooks/context.ts
105
+ import type { HookInput } from '@mauribadnights/clooks';
106
+
107
+ export default async function(input: HookInput) {
108
+ const toolName = input.tool_name ?? 'unknown';
109
+ const cwd = input.cwd;
110
+
111
+ // Return value becomes handler output
112
+ return {
113
+ additionalContext: `Tool ${toolName} executing in ${cwd}`
114
+ };
115
+ }
116
+ ```
117
+
118
+ > **Note:** The module must have a default export that is a function. If the export is missing or not a function, the handler fails with an error message identifying the module.
119
+
120
+ ## LLM Handlers
121
+
122
+ LLM handlers call the Anthropic Messages API with prompt templates. They require no scripts -- the prompt is defined directly in the manifest.
123
+
124
+ ### How They Work
125
+
126
+ 1. The handler's `prompt` template is rendered by replacing `$VARIABLES` with actual values.
127
+ 2. The rendered prompt is sent to the Anthropic API using the specified `model`.
128
+ 3. The response text is returned as `{"additionalContext": "..."}`.
129
+
130
+ ### Required Fields
131
+
132
+ | Field | Type | Description |
133
+ |-------|------|-------------|
134
+ | `model` | string | `claude-haiku-4-5`, `claude-sonnet-4-6`, or `claude-opus-4-6` |
135
+ | `prompt` | string | Prompt template with `$VARIABLE` interpolation |
136
+
137
+ ### Optional Fields
138
+
139
+ | Field | Type | Default | Description |
140
+ |-------|------|---------|-------------|
141
+ | `maxTokens` | number | 1024 | Maximum tokens in the API response |
142
+ | `temperature` | number | 1.0 | Sampling temperature |
143
+ | `batchGroup` | string | — | Group ID for batching multiple handlers into one API call |
144
+
145
+ ### Default Timeout
146
+
147
+ 30000ms. Override with the `timeout` field.
148
+
149
+ ### Prompt Variables
150
+
151
+ | Variable | Source |
152
+ |----------|--------|
153
+ | `$TRANSCRIPT` | Session transcript (requires `transcript` in prefetch) |
154
+ | `$GIT_STATUS` | `git status --porcelain` (requires `git_status` in prefetch) |
155
+ | `$GIT_DIFF` | `git diff --stat` (requires `git_diff` in prefetch) |
156
+ | `$ARGUMENTS` | JSON-serialized `tool_input` (PreToolUse/PostToolUse) |
157
+ | `$TOOL_NAME` | Tool name (PreToolUse/PostToolUse) |
158
+ | `$PROMPT` | User prompt text (UserPromptSubmit) |
159
+ | `$CWD` | Current working directory |
160
+
161
+ ### Example Configuration
162
+
163
+ ```yaml
164
+ prefetch:
165
+ - git_status
166
+
167
+ handlers:
168
+ PreToolUse:
169
+ - id: code-reviewer
170
+ type: llm
171
+ model: claude-haiku-4-5
172
+ prompt: |
173
+ Review this tool call for potential issues.
174
+ Tool: $TOOL_NAME
175
+ Arguments: $ARGUMENTS
176
+ Git status: $GIT_STATUS
177
+
178
+ If there is a problem, explain it briefly. Otherwise say "Looks good."
179
+ filter: "Write|Edit"
180
+ maxTokens: 256
181
+ ```
182
+
183
+ See [LLM Handlers](llm-handlers.md) for batching, cost tracking, and advanced usage.
184
+
185
+ ## Handler Output Format
186
+
187
+ Handlers communicate back to Claude Code through their output. There are two primary output shapes.
188
+
189
+ ### Adding Context
190
+
191
+ Return an `additionalContext` string to inject information into Claude's context window:
192
+
193
+ ```json
194
+ {
195
+ "additionalContext": "Information to inject into Claude's context"
196
+ }
197
+ ```
198
+
199
+ This is the most common output. Claude sees this text as additional context when deciding its next action.
200
+
201
+ ### Blocking a Tool (PreToolUse only)
202
+
203
+ PreToolUse handlers can block a tool call by returning a `decision` of `"block"`:
204
+
205
+ ```json
206
+ {
207
+ "decision": "block",
208
+ "reason": "This operation is not allowed because..."
209
+ }
210
+ ```
211
+
212
+ When a handler blocks a tool, Claude receives the reason and must find an alternative approach. Multiple handlers can run for the same event -- if any handler blocks, the tool is blocked.
213
+
214
+ ### No Output
215
+
216
+ Returning nothing (empty stdout for scripts, `undefined` for inline) is valid. The handler is recorded as successful with no output.
217
+
218
+ ## Auto-Disable
219
+
220
+ Handlers that fail repeatedly are automatically disabled to prevent cascading problems:
221
+
222
+ - After **3 consecutive failures**, the handler is marked as disabled.
223
+ - Disabled handlers are skipped on subsequent invocations (logged as auto-disabled).
224
+ - State is tracked per handler ID in memory.
225
+
226
+ To re-enable a disabled handler:
227
+
228
+ - **Edit the manifest** -- any manifest reload resets state for changed handlers.
229
+ - **Use `sessionIsolation: true`** -- state resets automatically on every `SessionStart` event.
230
+ - **Restart the daemon** -- `clooks restart` clears all in-memory state.
231
+
232
+ Check current handler status with `clooks stats`, which shows error counts and disabled state per handler.
233
+
234
+ ---
235
+
236
+ [Home](../index.md) | [Prev: Manifest](manifest.md) | [Next: LLM Handlers](llm-handlers.md)