@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.
- package/README.md +2 -0
- package/dist/cli.js +61 -10
- package/dist/handlers.d.ts +2 -0
- package/dist/handlers.js +43 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.js +120 -11
- package/docs/.nojekyll +0 -0
- package/docs/_sidebar.md +32 -0
- package/docs/getting-started/installation.md +52 -0
- package/docs/getting-started/migration.md +68 -0
- package/docs/getting-started/quickstart.md +76 -0
- package/docs/guides/async-handlers.md +42 -0
- package/docs/guides/dependencies.md +153 -0
- package/docs/guides/filtering.md +153 -0
- package/docs/guides/handlers.md +236 -0
- package/docs/guides/llm-handlers.md +145 -0
- package/docs/guides/manifest.md +237 -0
- package/docs/guides/short-circuit.md +31 -0
- package/docs/guides/system-service.md +62 -0
- package/docs/index.html +43 -0
- package/docs/index.md +96 -0
- package/docs/operations/architecture.md +105 -0
- package/docs/operations/monitoring.md +94 -0
- package/docs/operations/security.md +76 -0
- package/docs/operations/troubleshooting.md +123 -0
- package/docs/plugins/cc-plugin-import.md +55 -0
- package/docs/plugins/creating-plugins.md +144 -0
- package/docs/plugins/using-plugins.md +63 -0
- package/docs/reference/cli.md +213 -0
- package/docs/reference/config-files.md +129 -0
- package/docs/reference/hook-events.md +128 -0
- package/docs/reference/http-api.md +122 -0
- package/docs/reference/types.md +410 -0
- package/package.json +1 -1
|
@@ -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)
|