@mauribadnights/clooks 0.5.3 → 0.6.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 CHANGED
@@ -101,7 +101,7 @@ handlers:
101
101
  timeout: 3000
102
102
  enabled: true
103
103
 
104
- # LLM handler -- calls Anthropic Messages API
104
+ # LLM handler -- calls Anthropic Messages API (default backend)
105
105
  - id: code-review
106
106
  type: llm
107
107
  model: claude-haiku-4-5
@@ -119,6 +119,14 @@ handlers:
119
119
  batchGroup: analysis
120
120
  agent: "builder" # only fire in builder agent sessions
121
121
 
122
+ # LLM handler -- spawns Claude Code CLI with an agent
123
+ - id: agent-review
124
+ type: llm
125
+ backend: claude-code # spawn claude CLI instead of API call
126
+ llmAgent: security-reviewer # --agent flag
127
+ prompt: "Audit this change: $TOOL_NAME $ARGUMENTS"
128
+ filter: "Bash|Write"
129
+
122
130
  UserPromptSubmit:
123
131
  # Inline handler -- imports a JS module in-process (no subprocess)
124
132
  - id: prompt-logger
@@ -152,7 +160,7 @@ settings:
152
160
 
153
161
  **inline** -- imports a JS module and calls its default export. No subprocess overhead.
154
162
 
155
- **llm** -- calls Anthropic Messages API with `$VARIABLE` interpolation. Supports batching and cost tracking.
163
+ **llm** -- AI-powered analysis with `$VARIABLE` interpolation. Two backends: `api` (default, Anthropic API with batching and cost tracking) and `claude-code` (CLI spawn with agent support).
156
164
 
157
165
  ## Handler Fields Reference
158
166
 
@@ -162,15 +170,17 @@ settings:
162
170
  | `type` | string | required | all | `script`, `inline`, or `llm` |
163
171
  | `command` | string | required | script | Shell command to execute |
164
172
  | `module` | string | required | inline | Path to JS module with default export |
165
- | `model` | string | required | llm | `claude-haiku-4-5`, `claude-sonnet-4-6`, or `claude-opus-4-6` |
173
+ | `model` | string | required | llm | `claude-haiku-4-5`, `claude-sonnet-4-6`, or `claude-opus-4-6`. Required for `api` backend, optional for `claude-code`. |
166
174
  | `prompt` | string | required | llm | Prompt template with `$VARIABLE` interpolation |
175
+ | `backend` | string | `api` | llm | `api` (Anthropic API) or `claude-code` (CLI spawn) |
176
+ | `llmAgent` | string | -- | llm | Agent name for `claude-code` backend (`--agent` flag) |
167
177
  | `filter` | string | -- | all | Keyword filter (see Filtering) |
168
178
  | `project` | string | -- | all | Glob pattern matched against cwd |
169
179
  | `agent` | string | -- | all | Only fire when session agent matches |
170
180
  | `async` | boolean | `false` | all | Fire-and-forget, don't block response |
171
181
  | `depends` | string[] | -- | all | Handler IDs to wait for before executing |
172
182
  | `sessionIsolation` | boolean | `false` | all | Reset handler state on SessionStart |
173
- | `batchGroup` | string | -- | llm | Group ID for batching into one API call |
183
+ | `batchGroup` | string | -- | llm | Group ID for batching into one API call (`api` backend only) |
174
184
  | `maxTokens` | number | `1024` | llm | Maximum output tokens |
175
185
  | `temperature` | number | `1.0` | llm | Sampling temperature |
176
186
  | `timeout` | number | `5000`/`30000` | all | Timeout in ms (5s default, 30s for llm) |
@@ -213,13 +223,17 @@ filter: "word1|!word2" # run if word1 present AND word2 absent
213
223
 
214
224
  ## LLM Handlers
215
225
 
216
- **Setup:**
226
+ LLM handlers support two backends: `api` (default, Anthropic Messages API) and `claude-code` (spawns `claude` CLI).
227
+
228
+ **API backend setup:**
217
229
 
218
230
  ```bash
219
- npm install @anthropic-ai/sdk # peer dependency, required only for llm handlers
231
+ npm install @anthropic-ai/sdk # peer dependency, required only for api backend
220
232
  export ANTHROPIC_API_KEY=sk-... # or set in manifest: settings.anthropicApiKey
221
233
  ```
222
234
 
235
+ **Claude Code backend** requires no API key or SDK — just the `claude` CLI installed and authenticated. Supports the `llmAgent` field for running prompts with a specific agent (`--agent`).
236
+
223
237
  **Prompt template variables:**
224
238
 
225
239
  | Variable | Source | Description |
@@ -234,7 +248,7 @@ export ANTHROPIC_API_KEY=sk-... # or set in manifest: settings.anthropicApiKey
234
248
 
235
249
  `$TRANSCRIPT`, `$GIT_STATUS`, and `$GIT_DIFF` require the corresponding key in `prefetch`. The others are always available from the hook input.
236
250
 
237
- **Batching:** Handlers sharing a `batchGroup` on the same event are combined into a single API call. Three Haiku calls become one, saving ~2/3 of input token cost and eliminating two round-trips. Batch groups are scoped per session to prevent cross-session contamination.
251
+ **Batching (API backend only):** Handlers sharing a `batchGroup` on the same event are combined into a single API call. Three Haiku calls become one, saving ~2/3 of input token cost and eliminating two round-trips. Batch groups are scoped per session to prevent cross-session contamination. Claude Code backend handlers always execute individually.
238
252
 
239
253
  ## Async Handlers
240
254
 
@@ -408,7 +422,7 @@ src/
408
422
  manifest.ts Manifest loading and validation
409
423
  metrics.ts Metrics collection and aggregation
410
424
  tui.ts Interactive terminal dashboard (ANSI-based)
411
- llm.ts Anthropic API integration and batching
425
+ llm.ts LLM execution (Anthropic API + Claude Code CLI) and batching
412
426
  filter.ts Keyword filter engine
413
427
  prefetch.ts Pre-fetch context (transcript, git status/diff)
414
428
  plugin.ts Plugin install/remove/list
package/agents/clooks.md CHANGED
@@ -33,7 +33,7 @@ You can run clooks CLI commands:
33
33
  ### Handler types
34
34
  - **script** — spawns `sh -c "command"`, pipes hook JSON to stdin, reads stdout (~5-35ms)
35
35
  - **inline** — imports a JS module, calls default export in-process (~0ms after first load)
36
- - **llm** — calls Anthropic Messages API with prompt template and $VARIABLE interpolation
36
+ - **llm** — AI-powered analysis with prompt template and $VARIABLE interpolation. Two backends: `api` (Anthropic API, default) and `claude-code` (spawns `claude` CLI)
37
37
 
38
38
  ### Handler fields
39
39
  Every handler has:
@@ -49,9 +49,11 @@ Every handler has:
49
49
  - `enabled` — boolean
50
50
 
51
51
  ### LLM handler extra fields
52
- - `model` — claude-haiku-4-5, claude-sonnet-4-6, or claude-opus-4-6
52
+ - `model` — claude-haiku-4-5, claude-sonnet-4-6, or claude-opus-4-6 (required for `api` backend, optional for `claude-code`)
53
53
  - `prompt` — template with $TRANSCRIPT, $GIT_STATUS, $GIT_DIFF, $ARGUMENTS, $TOOL_NAME, $PROMPT, $CWD
54
- - `batchGroup` — handlers with same group + same session = one API call
54
+ - `backend` — `api` (default, Anthropic API) or `claude-code` (spawns `claude -p`)
55
+ - `llmAgent` — agent name for `claude-code` backend (passes `--agent` to CLI)
56
+ - `batchGroup` — handlers with same group + same session = one API call (`api` backend only)
55
57
  - `maxTokens`, `temperature`
56
58
 
57
59
  ### Plugin system
@@ -82,6 +84,13 @@ handlers:
82
84
  batchGroup: analysis
83
85
  async: true
84
86
 
87
+ - id: agent-review
88
+ type: llm
89
+ backend: claude-code
90
+ llmAgent: reviewer
91
+ prompt: "Review this prompt for clarity: $PROMPT"
92
+ async: true
93
+
85
94
  Stop:
86
95
  - id: session-logger
87
96
  type: inline
@@ -134,7 +143,8 @@ SessionStart includes a command hook for `clooks ensure-running` that auto-start
134
143
  - Mark non-blocking handlers as `async: true`
135
144
  - Use `filter` to skip irrelevant invocations
136
145
  - Use `project`/`agent` to scope handlers to relevant contexts
137
- - Batch LLM handlers with `batchGroup`
146
+ - Batch `api` backend LLM handlers with `batchGroup`
147
+ - Use `claude-code` backend when agent capabilities are needed or to avoid API key management
138
148
  - Use `prefetch` to avoid redundant file reads
139
149
 
140
150
  ### Writing new handlers
package/dist/llm.d.ts CHANGED
@@ -6,7 +6,7 @@ export declare function resetClient(): void;
6
6
  */
7
7
  export declare function calculateCost(model: string, usage: TokenUsage): number;
8
8
  /**
9
- * Execute a single LLM handler: render prompt, call Messages API, return result.
9
+ * Execute a single LLM handler, dispatching to the appropriate backend.
10
10
  */
11
11
  export declare function executeLLMHandler(handler: LLMHandlerConfig, input: HookInput, context: PrefetchContext): Promise<HandlerResult>;
12
12
  /**
package/dist/llm.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
- // clooks LLM handler execution — Anthropic Messages API with batching
2
+ // clooks LLM handler execution — Anthropic Messages API with batching, Claude Code CLI spawn
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.resetClient = resetClient;
5
5
  exports.calculateCost = calculateCost;
6
6
  exports.executeLLMHandler = executeLLMHandler;
7
7
  exports.executeLLMHandlersBatched = executeLLMHandlersBatched;
8
+ const child_process_1 = require("child_process");
8
9
  const prefetch_js_1 = require("./prefetch.js");
9
10
  const constants_js_1 = require("./constants.js");
10
11
  /** Lazy-loaded Anthropic SDK client */
@@ -44,17 +45,27 @@ function calculateCost(model, usage) {
44
45
  return inputCost + outputCost;
45
46
  }
46
47
  /**
47
- * Execute a single LLM handler: render prompt, call Messages API, return result.
48
+ * Execute a single LLM handler, dispatching to the appropriate backend.
48
49
  */
49
50
  async function executeLLMHandler(handler, input, context) {
51
+ if (handler.backend === 'claude-code') {
52
+ return executeClaudeCodeHandler(handler, input, context);
53
+ }
54
+ return executeAPIHandler(handler, input, context);
55
+ }
56
+ /**
57
+ * Execute via Anthropic Messages API.
58
+ */
59
+ async function executeAPIHandler(handler, input, context) {
50
60
  const start = performance.now();
51
61
  const timeout = handler.timeout ?? constants_js_1.DEFAULT_LLM_TIMEOUT;
52
62
  const maxTokens = handler.maxTokens ?? constants_js_1.DEFAULT_LLM_MAX_TOKENS;
53
63
  try {
54
64
  const client = await getClient();
55
65
  const prompt = (0, prefetch_js_1.renderPromptTemplate)(handler.prompt, input, context);
66
+ const model = handler.model; // Guaranteed by manifest validation for api backend
56
67
  const apiCall = client.messages.create({
57
- model: handler.model,
68
+ model,
58
69
  max_tokens: maxTokens,
59
70
  messages: [{ role: 'user', content: prompt }],
60
71
  });
@@ -65,7 +76,7 @@ async function executeLLMHandler(handler, input, context) {
65
76
  input_tokens: response.usage?.input_tokens ?? 0,
66
77
  output_tokens: response.usage?.output_tokens ?? 0,
67
78
  };
68
- const cost_usd = calculateCost(handler.model, usage);
79
+ const cost_usd = calculateCost(model, usage);
69
80
  return {
70
81
  id: handler.id,
71
82
  ok: true,
@@ -84,6 +95,72 @@ async function executeLLMHandler(handler, input, context) {
84
95
  };
85
96
  }
86
97
  }
98
+ /**
99
+ * Execute via Claude Code CLI spawn (`claude -p "prompt"`).
100
+ * Supports --agent and --model flags.
101
+ */
102
+ function executeClaudeCodeHandler(handler, input, context) {
103
+ const start = performance.now();
104
+ const timeout = handler.timeout ?? constants_js_1.DEFAULT_LLM_TIMEOUT;
105
+ const prompt = (0, prefetch_js_1.renderPromptTemplate)(handler.prompt, input, context);
106
+ const args = ['-p', prompt, '--output-format', 'text'];
107
+ if (handler.llmAgent) {
108
+ args.push('--agent', handler.llmAgent);
109
+ }
110
+ if (handler.model) {
111
+ args.push('--model', handler.model);
112
+ }
113
+ if (handler.maxTokens) {
114
+ args.push('--max-tokens', String(handler.maxTokens));
115
+ }
116
+ return new Promise((resolve) => {
117
+ const child = (0, child_process_1.spawn)('claude', args, {
118
+ stdio: ['pipe', 'pipe', 'pipe'],
119
+ timeout,
120
+ env: { ...process.env },
121
+ });
122
+ let stdout = '';
123
+ let stderr = '';
124
+ child.stdout.on('data', (data) => {
125
+ stdout += data.toString();
126
+ });
127
+ child.stderr.on('data', (data) => {
128
+ stderr += data.toString();
129
+ });
130
+ child.stdin.end();
131
+ const timer = setTimeout(() => {
132
+ child.kill('SIGTERM');
133
+ }, timeout);
134
+ child.on('close', (code) => {
135
+ clearTimeout(timer);
136
+ const duration_ms = performance.now() - start;
137
+ if (code !== 0) {
138
+ resolve({
139
+ id: handler.id,
140
+ ok: false,
141
+ error: `claude exit code ${code}${stderr ? ': ' + stderr.trim() : ''}`,
142
+ duration_ms,
143
+ });
144
+ return;
145
+ }
146
+ resolve({
147
+ id: handler.id,
148
+ ok: true,
149
+ output: { additionalContext: stdout.trim() },
150
+ duration_ms,
151
+ });
152
+ });
153
+ child.on('error', (err) => {
154
+ clearTimeout(timer);
155
+ resolve({
156
+ id: handler.id,
157
+ ok: false,
158
+ error: `Claude Code spawn error: ${err.message}`,
159
+ duration_ms: performance.now() - start,
160
+ });
161
+ });
162
+ });
163
+ }
87
164
  /**
88
165
  * Execute a batched group of LLM handlers: combine prompts into a single
89
166
  * multi-task API call, parse JSON response back into individual results.
@@ -91,7 +168,7 @@ async function executeLLMHandler(handler, input, context) {
91
168
  async function executeBatchGroup(handlers, input, context) {
92
169
  const start = performance.now();
93
170
  // Use model from first handler; warn if others differ
94
- const model = handlers[0].model;
171
+ const model = handlers[0].model; // Guaranteed by manifest validation — batching only applies to api backend
95
172
  for (let i = 1; i < handlers.length; i++) {
96
173
  if (handlers[i].model !== model) {
97
174
  console.warn(`[clooks] Batch group "${handlers[0].batchGroup}": handler "${handlers[i].id}" ` +
@@ -192,10 +269,14 @@ function splitUsage(total, count) {
192
269
  */
193
270
  async function executeLLMHandlersBatched(handlers, input, context, sessionId) {
194
271
  // Group by batchGroup, scoped by sessionId to prevent cross-session batching
272
+ // claude-code handlers can't be batched — always run individually
195
273
  const grouped = new Map();
196
274
  const ungrouped = [];
197
275
  for (const handler of handlers) {
198
- if (handler.batchGroup) {
276
+ if (handler.backend === 'claude-code') {
277
+ ungrouped.push(handler);
278
+ }
279
+ else if (handler.batchGroup) {
199
280
  // Scope the batch key by sessionId so different sessions never batch together
200
281
  const batchKey = sessionId
201
282
  ? `${handler.batchGroup}:${sessionId}`
package/dist/manifest.js CHANGED
@@ -63,15 +63,38 @@ function validateManifest(manifest) {
63
63
  }
64
64
  if (handler.type === 'llm') {
65
65
  const llm = handler;
66
- if (!llm.model) {
67
- throw new Error(`LLM handler "${handler.id}" must have a "model" field`);
68
- }
69
66
  if (!llm.prompt) {
70
67
  throw new Error(`LLM handler "${handler.id}" must have a "prompt" field`);
71
68
  }
72
- const validModels = ['claude-haiku-4-5', 'claude-sonnet-4-6', 'claude-opus-4-6'];
73
- if (!validModels.includes(llm.model)) {
74
- throw new Error(`LLM handler "${handler.id}" model must be one of: ${validModels.join(', ')}`);
69
+ // Validate backend
70
+ const validBackends = ['api', 'claude-code'];
71
+ if (llm.backend && !validBackends.includes(llm.backend)) {
72
+ throw new Error(`LLM handler "${handler.id}" backend must be one of: ${validBackends.join(', ')}`);
73
+ }
74
+ // llmAgent is only valid with claude-code backend
75
+ if (llm.llmAgent && llm.backend !== 'claude-code') {
76
+ throw new Error(`LLM handler "${handler.id}" llmAgent requires backend: claude-code`);
77
+ }
78
+ // model is required for api backend, optional for claude-code
79
+ if (llm.backend !== 'claude-code') {
80
+ if (!llm.model) {
81
+ throw new Error(`LLM handler "${handler.id}" must have a "model" field`);
82
+ }
83
+ const validModels = ['claude-haiku-4-5', 'claude-sonnet-4-6', 'claude-opus-4-6'];
84
+ if (!validModels.includes(llm.model)) {
85
+ throw new Error(`LLM handler "${handler.id}" model must be one of: ${validModels.join(', ')}`);
86
+ }
87
+ }
88
+ else if (llm.model) {
89
+ // claude-code backend with explicit model — still validate it
90
+ const validModels = ['claude-haiku-4-5', 'claude-sonnet-4-6', 'claude-opus-4-6'];
91
+ if (!validModels.includes(llm.model)) {
92
+ throw new Error(`LLM handler "${handler.id}" model must be one of: ${validModels.join(', ')}`);
93
+ }
94
+ }
95
+ // batchGroup is incompatible with claude-code backend
96
+ if (llm.batchGroup && llm.backend === 'claude-code') {
97
+ console.warn(`[clooks] Warning: LLM handler "${handler.id}" has batchGroup but uses claude-code backend — batching will be ignored`);
75
98
  }
76
99
  }
77
100
  // Validate async field type
package/dist/server.js CHANGED
@@ -91,11 +91,15 @@ function readBody(req) {
91
91
  }
92
92
  function sendJson(res, status, data) {
93
93
  const body = JSON.stringify(data);
94
- res.writeHead(status, {
95
- 'Content-Type': 'application/json',
96
- 'Content-Length': Buffer.byteLength(body),
97
- });
98
- res.end(body);
94
+ res.socket?.on('error', () => { }); // suppress EPIPE if client disconnected early
95
+ try {
96
+ res.writeHead(status, {
97
+ 'Content-Type': 'application/json',
98
+ 'Content-Length': Buffer.byteLength(body),
99
+ });
100
+ res.end(body);
101
+ }
102
+ catch (_) { }
99
103
  }
100
104
  /**
101
105
  * Create the HTTP server for hook handling.
package/dist/types.d.ts CHANGED
@@ -17,14 +17,18 @@ export interface HookInput {
17
17
  }
18
18
  /** Supported LLM models */
19
19
  export type LLMModel = 'claude-haiku-4-5' | 'claude-sonnet-4-6' | 'claude-opus-4-6';
20
+ /** LLM execution backend */
21
+ export type LLMBackend = 'api' | 'claude-code';
20
22
  /** Handler types — extended with 'llm' */
21
23
  export type HandlerType = 'script' | 'inline' | 'llm';
22
24
  /** LLM-specific handler config fields */
23
25
  export interface LLMHandlerConfig {
24
26
  id: string;
25
27
  type: 'llm';
26
- model: LLMModel;
28
+ model?: LLMModel;
27
29
  prompt: string;
30
+ backend?: LLMBackend;
31
+ llmAgent?: string;
28
32
  batchGroup?: string;
29
33
  maxTokens?: number;
30
34
  temperature?: number;
@@ -68,7 +68,7 @@ This launches an interactive TUI showing execution counts, latency, and errors p
68
68
  ## What to try next
69
69
 
70
70
  - Add a `filter` to scope handlers to specific tools
71
- - Try an `llm` handler for AI-powered review
71
+ - Try an `llm` handler for AI-powered review (use `backend: claude-code` to skip API key setup)
72
72
  - Run `clooks migrate` to convert existing command hooks
73
73
 
74
74
  ---
@@ -119,28 +119,31 @@ export default async function(input: HookInput) {
119
119
 
120
120
  ## LLM Handlers
121
121
 
122
- LLM handlers call the Anthropic Messages API with prompt templates. They require no scripts -- the prompt is defined directly in the manifest.
122
+ LLM handlers run AI-powered analysis with prompt templates. They support two backends: the Anthropic Messages API (`api`, default) and Claude Code CLI spawn (`claude-code`).
123
123
 
124
124
  ### How They Work
125
125
 
126
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": "..."}`.
127
+ 2. **API backend:** The rendered prompt is sent to the Anthropic API using the specified `model`.
128
+ 3. **Claude Code backend:** The rendered prompt is passed to `claude -p`, optionally with `--agent` and `--model`.
129
+ 4. The response text is returned as `{"additionalContext": "..."}`.
129
130
 
130
131
  ### Required Fields
131
132
 
132
133
  | Field | Type | Description |
133
134
  |-------|------|-------------|
134
- | `model` | string | `claude-haiku-4-5`, `claude-sonnet-4-6`, or `claude-opus-4-6` |
135
135
  | `prompt` | string | Prompt template with `$VARIABLE` interpolation |
136
+ | `model` | string | `claude-haiku-4-5`, `claude-sonnet-4-6`, or `claude-opus-4-6`. Required for `api` backend, optional for `claude-code`. |
136
137
 
137
138
  ### Optional Fields
138
139
 
139
140
  | Field | Type | Default | Description |
140
141
  |-------|------|---------|-------------|
141
- | `maxTokens` | number | 1024 | Maximum tokens in the API response |
142
+ | `backend` | string | `api` | `api` (Anthropic API) or `claude-code` (CLI spawn) |
143
+ | `llmAgent` | string | — | Agent name for `claude-code` backend (`--agent` flag) |
144
+ | `maxTokens` | number | 1024 | Maximum tokens in the response |
142
145
  | `temperature` | number | 1.0 | Sampling temperature |
143
- | `batchGroup` | string | — | Group ID for batching multiple handlers into one API call |
146
+ | `batchGroup` | string | — | Group ID for batching into one API call (`api` backend only) |
144
147
 
145
148
  ### Default Timeout
146
149
 
@@ -166,6 +169,7 @@ prefetch:
166
169
 
167
170
  handlers:
168
171
  PreToolUse:
172
+ # API backend (default) — fast, supports batching and cost tracking
169
173
  - id: code-reviewer
170
174
  type: llm
171
175
  model: claude-haiku-4-5
@@ -178,9 +182,17 @@ handlers:
178
182
  If there is a problem, explain it briefly. Otherwise say "Looks good."
179
183
  filter: "Write|Edit"
180
184
  maxTokens: 256
185
+
186
+ # Claude Code backend — supports agents, no API key needed
187
+ - id: agent-review
188
+ type: llm
189
+ backend: claude-code
190
+ llmAgent: security-reviewer
191
+ prompt: "Audit this tool call for security issues: $TOOL_NAME $ARGUMENTS"
192
+ filter: "Bash|Write"
181
193
  ```
182
194
 
183
- See [LLM Handlers](llm-handlers.md) for batching, cost tracking, and advanced usage.
195
+ See [LLM Handlers](llm-handlers.md) for backends, batching, cost tracking, and advanced usage.
184
196
 
185
197
  ## Handler Output Format
186
198
 
@@ -1,16 +1,57 @@
1
1
  # LLM Handlers
2
2
 
3
- LLM handlers call the Anthropic Messages API directly from the manifest, with prompt templates, automatic batching, and cost tracking. This guide covers advanced usage beyond the basics in [Handlers](handlers.md).
3
+ LLM handlers run AI-powered analysis from the manifest, with prompt templates, automatic batching, and cost tracking. Two backends are available: the **Anthropic Messages API** (default) and **Claude Code CLI** spawn. This guide covers advanced usage beyond the basics in [Handlers](handlers.md).
4
4
 
5
- ## Basics
5
+ ## Backends
6
6
 
7
- LLM handlers require an Anthropic API key. Provide it in one of two ways:
7
+ LLM handlers support two execution backends via the `backend` field:
8
+
9
+ | Backend | Value | Description |
10
+ |---------|-------|-------------|
11
+ | Anthropic API | `api` (default) | Direct API call. Supports batching, cost tracking, token usage. Requires API key and SDK. |
12
+ | Claude Code CLI | `claude-code` | Spawns `claude -p "prompt"`. Supports agents. No API key needed — uses your Claude Code subscription. |
13
+
14
+ ### API Backend (default)
15
+
16
+ The `api` backend requires an Anthropic API key. Provide it in one of two ways:
8
17
 
9
18
  1. **Environment variable:** `ANTHROPIC_API_KEY=sk-ant-...`
10
19
  2. **Manifest setting:** `settings.anthropicApiKey: sk-ant-...`
11
20
 
12
21
  The Anthropic SDK is lazy-loaded on the first LLM handler invocation. If the SDK is not installed, the handler fails with an actionable error message.
13
22
 
23
+ ### Claude Code Backend
24
+
25
+ The `claude-code` backend spawns a `claude` CLI process. It requires the Claude Code CLI to be installed and authenticated. No API key or SDK is needed.
26
+
27
+ ```yaml
28
+ - id: deep-review
29
+ type: llm
30
+ backend: claude-code
31
+ prompt: "Review this tool call: $TOOL_NAME $ARGUMENTS"
32
+ ```
33
+
34
+ Key differences from the API backend:
35
+
36
+ - **No cost tracking** — usage is billed through your Claude Code subscription, not per-token.
37
+ - **No batching** — each handler spawns its own `claude` process. `batchGroup` is ignored.
38
+ - **Agent support** — use `llmAgent` to run the prompt with a specific agent.
39
+ - **Model is optional** — omit `model` to use Claude Code's default, or specify one to override.
40
+
41
+ #### Using Agents
42
+
43
+ The `llmAgent` field passes `--agent <name>` to the Claude Code CLI, running the prompt with a specific agent's instructions and tools:
44
+
45
+ ```yaml
46
+ - id: agent-review
47
+ type: llm
48
+ backend: claude-code
49
+ llmAgent: security-reviewer
50
+ prompt: "Audit this change for vulnerabilities: $ARGUMENTS"
51
+ ```
52
+
53
+ > **Note:** `llmAgent` is only valid with `backend: claude-code`. Using it with the `api` backend produces a validation error.
54
+
14
55
  ### Supported Models
15
56
 
16
57
  | Model | Best For |
@@ -19,6 +60,8 @@ The Anthropic SDK is lazy-loaded on the first LLM handler invocation. If the SDK
19
60
  | `claude-sonnet-4-6` | Balanced analysis (code review, context synthesis) |
20
61
  | `claude-opus-4-6` | Deep reasoning (security audits, architecture review) |
21
62
 
63
+ `model` is required for the `api` backend. For `claude-code`, it is optional — when provided, it is passed as `--model` to the CLI.
64
+
22
65
  ## Prompt Templates
23
66
 
24
67
  Prompts support `$VARIABLE` interpolation. Variables are replaced with actual values before the API call.
@@ -56,9 +99,9 @@ handlers:
56
99
  Flag any issues. Be concise.
57
100
  ```
58
101
 
59
- ## Batching
102
+ ## Batching (API Backend Only)
60
103
 
61
- Handlers with the same `batchGroup` value that fire on the same event are combined into a single API call. This reduces latency and cost when multiple LLM handlers need to analyze the same context.
104
+ Handlers with the same `batchGroup` value that fire on the same event are combined into a single API call. This reduces latency and cost when multiple LLM handlers need to analyze the same context. Batching only applies to handlers using the `api` backend — `claude-code` handlers always execute individually.
62
105
 
63
106
  ### How It Works
64
107
 
@@ -128,15 +171,28 @@ Pricing per million tokens (as of March 2026):
128
171
 
129
172
  For batched calls, the total token cost is split evenly across all handlers in the group.
130
173
 
174
+ ## Choosing a Backend
175
+
176
+ | Consideration | `api` | `claude-code` |
177
+ |---------------|-------|---------------|
178
+ | Latency | Lower (direct HTTP) | Higher (process spawn) |
179
+ | Cost model | Per-token (tracked) | Subscription (not tracked) |
180
+ | Batching | Yes | No |
181
+ | Agent support | No | Yes (`llmAgent`) |
182
+ | Requires API key | Yes | No |
183
+ | Requires SDK | Yes | No |
184
+
185
+ **Use `api`** for high-frequency, latency-sensitive handlers (guards, quick checks). **Use `claude-code`** when you need agent capabilities, don't want to manage API keys, or want handlers to use your subscription.
186
+
131
187
  ## Best Practices
132
188
 
133
189
  **Use Haiku for simple checks.** Guards, keyword detection, and light reviews run well on Haiku at a fraction of the cost. Reserve Sonnet and Opus for tasks that require deeper reasoning.
134
190
 
135
- **Use batchGroup to combine related analyses.** If two handlers analyze the same tool call from different angles, batching them saves an API round-trip and reduces total tokens (the shared context is sent once).
191
+ **Use batchGroup to combine related analyses.** If two handlers analyze the same tool call from different angles, batching them saves an API round-trip and reduces total tokens (the shared context is sent once). Only applies to the `api` backend.
136
192
 
137
193
  **Set maxTokens conservatively.** Most handler responses are short. Setting `maxTokens: 256` or `maxTokens: 512` prevents runaway token usage on verbose responses.
138
194
 
139
- **Use filter to avoid unnecessary API calls.** An LLM handler without a filter fires on every matching event. Adding `filter: "Write|Edit"` ensures the API is only called when relevant tools are invoked.
195
+ **Use filter to avoid unnecessary calls.** An LLM handler without a filter fires on every matching event. Adding `filter: "Write|Edit"` ensures the handler is only invoked when relevant tools are used.
140
196
 
141
197
  **Prefer prefetch over inline context.** If your prompt needs git status or the transcript, add the key to `prefetch` rather than running shell commands in a script handler. Prefetched data is fetched once and shared across all handlers.
142
198
 
@@ -137,7 +137,9 @@ Run `clooks doctor` to validate your manifest structure. The validator enforces:
137
137
  - Handler IDs must be unique across the entire manifest (not just within an event).
138
138
  - Script handlers must have a `command` field.
139
139
  - Inline handlers must have a `module` field.
140
- - LLM handlers must have both `model` and `prompt` fields. Model must be one of: `claude-haiku-4-5`, `claude-sonnet-4-6`, `claude-opus-4-6`.
140
+ - LLM handlers must have a `prompt` field. The `model` field is required for the `api` backend (default) and optional for `claude-code`. Model must be one of: `claude-haiku-4-5`, `claude-sonnet-4-6`, `claude-opus-4-6`.
141
+ - LLM handler `backend` must be `api` or `claude-code` (if specified).
142
+ - `llmAgent` is only valid when `backend` is `claude-code`.
141
143
  - `prefetch` must be an array containing only valid keys: `transcript`, `git_status`, `git_diff`.
142
144
  - `settings.port` must be a number between 1 and 65535.
143
145
  - `settings.logLevel` must be one of: `debug`, `info`, `warn`, `error`.
@@ -211,6 +213,13 @@ handlers:
211
213
  depends: [write-review, style-check]
212
214
  filter: "Write"
213
215
 
216
+ - id: agent-audit
217
+ type: llm
218
+ backend: claude-code
219
+ llmAgent: security-reviewer
220
+ prompt: "Audit this Bash command for security: $ARGUMENTS"
221
+ filter: "Bash"
222
+
214
223
  PostToolUse:
215
224
  - id: metrics-collector
216
225
  type: inline
package/docs/index.md CHANGED
@@ -53,7 +53,7 @@ docs/
53
53
 
54
54
  - [Manifest](guides/manifest.md) -- manifest.yaml structure and fields
55
55
  - [Handlers](guides/handlers.md) -- script, inline, and LLM handler types
56
- - [LLM Handlers](guides/llm-handlers.md) -- prompt templates, batching, cost tracking
56
+ - [LLM Handlers](guides/llm-handlers.md) -- prompt templates, batching, cost tracking, Claude Code CLI backend
57
57
  - [Filtering](guides/filtering.md) -- keyword-based handler filtering
58
58
  - [Dependencies](guides/dependencies.md) -- topological execution waves
59
59
  - [Async Handlers](guides/async-handlers.md) -- fire-and-forget execution
@@ -38,7 +38,8 @@ clooks daemon (localhost:7890)
38
38
  +-- Execute each wave
39
39
  | +-- Script handlers: spawn sh -c, pipe JSON stdin
40
40
  | +-- Inline handlers: dynamic ES module import
41
- | +-- LLM handlers: Anthropic API (batched by group)
41
+ | +-- LLM handlers (api): Anthropic API (batched by group)
42
+ | +-- LLM handlers (claude-code): spawn claude CLI (with optional --agent)
42
43
  |
43
44
  +-- Record metrics + costs
44
45
  +-- Track deny decisions (short-circuit cache)
@@ -68,9 +69,9 @@ Dependency resolution produces "waves" — groups of handlers that can run in pa
68
69
  - **Correct ordering** across waves
69
70
  - **Data flow** — outputs from earlier waves are available to later waves
70
71
 
71
- ### LLM Batching
72
+ ### LLM Batching (API Backend)
72
73
 
73
- Multiple LLM handlers with the same `batchGroup` are combined into a single API call. One prompt with multiple tasks, one response parsed and distributed. This typically halves cost and latency for multi-handler analysis.
74
+ Multiple LLM handlers using the `api` backend with the same `batchGroup` are combined into a single API call. One prompt with multiple tasks, one response parsed and distributed. This typically halves cost and latency for multi-handler analysis. Handlers using the `claude-code` backend always execute individually.
74
75
 
75
76
  ### Hot Reload
76
77
 
@@ -90,7 +91,8 @@ When a PreToolUse handler blocks a tool, PostToolUse is skipped. The denial is c
90
91
  - **Main process:** HTTP server, manifest loading, file watching, metric collection
91
92
  - **Script handlers:** Spawned as child processes (`sh -c`), piped JSON on stdin/stdout
92
93
  - **Inline handlers:** Loaded as ES modules in the main process (no subprocess)
93
- - **LLM handlers:** Async HTTP calls to Anthropic API from the main process
94
+ - **LLM handlers (api):** Async HTTP calls to Anthropic API from the main process
95
+ - **LLM handlers (claude-code):** Spawned as `claude -p` child processes (with optional `--agent`)
94
96
 
95
97
  ## Resilience
96
98
 
@@ -37,7 +37,7 @@ clooks stats | less # Auto-detects piped output, switches to text
37
37
 
38
38
  ## Cost Tracking
39
39
 
40
- LLM handler costs are logged to `~/.clooks/costs.jsonl`, rotated at 1MB.
40
+ LLM handler costs (API backend only) are logged to `~/.clooks/costs.jsonl`, rotated at 1MB. Handlers using the `claude-code` backend do not produce cost entries — usage is billed through the Claude Code subscription.
41
41
 
42
42
  ```bash
43
43
  clooks costs
@@ -71,7 +71,7 @@ Use `$PLUGIN_DIR` in `command` and `module` paths. When the plugin is installed,
71
71
 
72
72
  - `description` (string) — Shown in `clooks plugins` output
73
73
  - `author` (string) — Plugin author
74
- - `handlers` — Same format as user manifest handlers (all 3 types supported)
74
+ - `handlers` — Same format as user manifest handlers (all 3 types supported, including both `api` and `claude-code` LLM backends)
75
75
  - `prefetch` — Keys to pre-fetch (merged with user manifest)
76
76
  - `extras` — Freeform metadata:
77
77
  - `skills` (string[]) — Skill names the plugin provides
@@ -64,7 +64,7 @@ All configuration, state, and data files used by clooks, with their locations, f
64
64
 
65
65
  ## Cost File Format
66
66
 
67
- `costs.jsonl` tracks LLM-specific cost data, one entry per LLM handler invocation.
67
+ `costs.jsonl` tracks LLM-specific cost data, one entry per LLM handler invocation using the `api` backend. Handlers using the `claude-code` backend do not produce cost entries.
68
68
 
69
69
  ```json
70
70
  {
@@ -90,11 +90,13 @@ interface InlineHandlerConfig extends BaseHandler {
90
90
 
91
91
  interface LLMHandlerConfig extends BaseHandler {
92
92
  type: 'llm';
93
- model: LLMModel;
93
+ model?: LLMModel; // Required for 'api' backend, optional for 'claude-code'
94
94
  prompt: string;
95
- batchGroup?: string;
96
- maxTokens?: number; // Default: 1024
97
- temperature?: number; // Default: 1.0
95
+ backend?: LLMBackend; // Default: 'api'
96
+ llmAgent?: string; // Agent name for 'claude-code' backend (--agent flag)
97
+ batchGroup?: string; // 'api' backend only
98
+ maxTokens?: number; // Default: 1024
99
+ temperature?: number; // Default: 1.0
98
100
  }
99
101
 
100
102
  type HandlerConfig = ScriptHandlerConfig | InlineHandlerConfig | LLMHandlerConfig;
@@ -106,6 +108,15 @@ type HandlerConfig = ScriptHandlerConfig | InlineHandlerConfig | LLMHandlerConfi
106
108
  type LLMModel = 'claude-haiku-4-5' | 'claude-sonnet-4-6' | 'claude-opus-4-6';
107
109
  ```
108
110
 
111
+ ### LLMBackend
112
+
113
+ ```typescript
114
+ type LLMBackend = 'api' | 'claude-code';
115
+ ```
116
+
117
+ - `api` — Direct Anthropic Messages API call. Supports batching and cost tracking. Requires `ANTHROPIC_API_KEY` and the `@anthropic-ai/sdk` package.
118
+ - `claude-code` — Spawns `claude -p "prompt"`. Supports `llmAgent` for agent-based execution. No API key or SDK required.
119
+
109
120
  ### HandlerResult
110
121
 
111
122
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mauribadnights/clooks",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
4
4
  "description": "Persistent hook runtime for Claude Code — eliminates process spawning overhead and gives you observability",
5
5
  "bin": {
6
6
  "clooks": "./dist/cli.js"