@mauribadnights/clooks 0.5.1 → 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 +0 -0
- package/dist/handlers.d.ts +2 -0
- package/dist/handlers.js +43 -0
- 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
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@ Persistent hook runtime for Claude Code. Eliminate cold starts. Get observabilit
|
|
|
5
5
|
[](https://www.npmjs.com/package/@mauribadnights/clooks)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
+
**[Documentation](https://mauribadnights.github.io/clooks/)**
|
|
9
|
+
|
|
8
10
|
## Performance
|
|
9
11
|
|
|
10
12
|
| Metric | Without clooks | With clooks | Improvement |
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/handlers.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { HandlerConfig, HandlerResult, HandlerState, HookEvent, HookInput, PrefetchContext } from './types.js';
|
|
2
|
+
/** Reset cached shell env (for testing) */
|
|
3
|
+
export declare function resetShellEnv(): void;
|
|
2
4
|
/** Match handler agent field against current session agent (case-insensitive, comma-separated) */
|
|
3
5
|
declare function matchAgent(pattern: string, currentAgent: string): boolean;
|
|
4
6
|
/** Match handler project field against cwd path */
|
package/dist/handlers.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// clooks hook handlers — execution engine
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.resetShellEnv = resetShellEnv;
|
|
4
5
|
exports.matchAgent = matchAgent;
|
|
5
6
|
exports.matchProject = matchProject;
|
|
6
7
|
exports.resetHandlerStates = resetHandlerStates;
|
|
@@ -17,6 +18,47 @@ const constants_js_1 = require("./constants.js");
|
|
|
17
18
|
const filter_js_1 = require("./filter.js");
|
|
18
19
|
const llm_js_1 = require("./llm.js");
|
|
19
20
|
const deps_js_1 = require("./deps.js");
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the user's login shell PATH once at startup.
|
|
23
|
+
* When the daemon runs under launchd/systemd, it inherits a minimal PATH
|
|
24
|
+
* that may not include /opt/homebrew/bin, pyenv shims, nvm dirs, etc.
|
|
25
|
+
* This ensures script handlers see the same PATH as the user's terminal.
|
|
26
|
+
*/
|
|
27
|
+
let _shellEnv = null;
|
|
28
|
+
function getShellEnv() {
|
|
29
|
+
if (_shellEnv)
|
|
30
|
+
return _shellEnv;
|
|
31
|
+
try {
|
|
32
|
+
const shell = process.env.SHELL || '/bin/sh';
|
|
33
|
+
const output = (0, child_process_1.execSync)(`${shell} -ilc 'env'`, {
|
|
34
|
+
timeout: 5000,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
37
|
+
});
|
|
38
|
+
const env = {};
|
|
39
|
+
for (const line of output.split('\n')) {
|
|
40
|
+
const idx = line.indexOf('=');
|
|
41
|
+
if (idx > 0) {
|
|
42
|
+
env[line.slice(0, idx)] = line.slice(idx + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Merge: shell env as base, but keep daemon-specific vars (like ANTHROPIC_API_KEY)
|
|
46
|
+
_shellEnv = { ...env, ...process.env };
|
|
47
|
+
// PATH specifically: prefer the shell's PATH (has homebrew, pyenv, nvm, etc.)
|
|
48
|
+
if (env.PATH) {
|
|
49
|
+
_shellEnv.PATH = env.PATH;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Fallback: just use process.env as-is
|
|
54
|
+
_shellEnv = process.env;
|
|
55
|
+
}
|
|
56
|
+
return _shellEnv;
|
|
57
|
+
}
|
|
58
|
+
/** Reset cached shell env (for testing) */
|
|
59
|
+
function resetShellEnv() {
|
|
60
|
+
_shellEnv = null;
|
|
61
|
+
}
|
|
20
62
|
/** Match handler agent field against current session agent (case-insensitive, comma-separated) */
|
|
21
63
|
function matchAgent(pattern, currentAgent) {
|
|
22
64
|
const agents = pattern.split(',').map(a => a.trim().toLowerCase());
|
|
@@ -313,6 +355,7 @@ function executeScriptHandler(handler, input) {
|
|
|
313
355
|
const child = (0, child_process_1.spawn)('sh', ['-c', h.command], {
|
|
314
356
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
315
357
|
timeout,
|
|
358
|
+
env: getShellEnv(),
|
|
316
359
|
});
|
|
317
360
|
let stdout = '';
|
|
318
361
|
let stderr = '';
|
package/docs/.nojekyll
ADDED
|
File without changes
|
package/docs/_sidebar.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
- **Getting Started**
|
|
2
|
+
- [Installation](getting-started/installation.md)
|
|
3
|
+
- [Quickstart](getting-started/quickstart.md)
|
|
4
|
+
- [Migration](getting-started/migration.md)
|
|
5
|
+
|
|
6
|
+
- **Guides**
|
|
7
|
+
- [Manifest](guides/manifest.md)
|
|
8
|
+
- [Handlers](guides/handlers.md)
|
|
9
|
+
- [LLM Handlers](guides/llm-handlers.md)
|
|
10
|
+
- [Filtering](guides/filtering.md)
|
|
11
|
+
- [Dependencies](guides/dependencies.md)
|
|
12
|
+
- [Async Handlers](guides/async-handlers.md)
|
|
13
|
+
- [Short-Circuit](guides/short-circuit.md)
|
|
14
|
+
- [System Service](guides/system-service.md)
|
|
15
|
+
|
|
16
|
+
- **Plugins**
|
|
17
|
+
- [Using Plugins](plugins/using-plugins.md)
|
|
18
|
+
- [Creating Plugins](plugins/creating-plugins.md)
|
|
19
|
+
- [CC Plugin Import](plugins/cc-plugin-import.md)
|
|
20
|
+
|
|
21
|
+
- **Reference**
|
|
22
|
+
- [CLI](reference/cli.md)
|
|
23
|
+
- [Hook Events](reference/hook-events.md)
|
|
24
|
+
- [HTTP API](reference/http-api.md)
|
|
25
|
+
- [Config Files](reference/config-files.md)
|
|
26
|
+
- [Types](reference/types.md)
|
|
27
|
+
|
|
28
|
+
- **Operations**
|
|
29
|
+
- [Monitoring](operations/monitoring.md)
|
|
30
|
+
- [Security](operations/security.md)
|
|
31
|
+
- [Troubleshooting](operations/troubleshooting.md)
|
|
32
|
+
- [Architecture](operations/architecture.md)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- **Node.js 18+** -- check with `node --version`
|
|
6
|
+
- **Claude Code** -- installed and working (`claude --version`)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @mauribadnights/clooks
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Initialize
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
clooks init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This creates your configuration directory and everything clooks needs to run. No existing hooks are modified -- use `clooks migrate` for that (see [Migration](migration.md)).
|
|
21
|
+
|
|
22
|
+
## Verify
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
clooks doctor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Doctor runs health checks on the daemon, port, manifest, settings, and handler state. A passing report means clooks is ready.
|
|
29
|
+
|
|
30
|
+
## What `clooks init` creates
|
|
31
|
+
|
|
32
|
+
| Item | Path | Description |
|
|
33
|
+
|------|------|-------------|
|
|
34
|
+
| Manifest | `~/.clooks/manifest.yaml` | Handler definitions, settings, and prefetch config |
|
|
35
|
+
| Hooks directory | `~/.clooks/hooks/` | Built-in hook scripts |
|
|
36
|
+
| Auth token | Stored in manifest | Generated once and shown at init. Used to authenticate requests to the daemon. |
|
|
37
|
+
| System service | launchd (macOS) / systemd (Linux) | Auto-starts the daemon on login, restarts on crash |
|
|
38
|
+
| Expert agent | `~/.claude/agents/clooks.md` | Invoke with `claude --agent clooks` for clooks-specific help |
|
|
39
|
+
|
|
40
|
+
> **Note:** The auth token is displayed once during init. It is stored in your manifest under `settings.authToken`. If you lose it, run `clooks rotate-token` to generate a new one.
|
|
41
|
+
|
|
42
|
+
## Next steps
|
|
43
|
+
|
|
44
|
+
Start the daemon and add your first handler:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
clooks start
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
[Home](../index.md) | Next: [Quickstart](quickstart.md)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Migration
|
|
2
|
+
|
|
3
|
+
Migrate existing Claude Code command hooks to clooks in one command.
|
|
4
|
+
|
|
5
|
+
## What migration does
|
|
6
|
+
|
|
7
|
+
`clooks migrate` converts your `settings.json` command hooks into clooks HTTP hooks backed by the daemon, with equivalent handlers defined in the manifest. Your original command hooks continue to work -- they just route through clooks instead of spawning processes directly.
|
|
8
|
+
|
|
9
|
+
## Run the migration
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
clooks migrate
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Step-by-step breakdown
|
|
16
|
+
|
|
17
|
+
When you run `clooks migrate`, the following happens in order:
|
|
18
|
+
|
|
19
|
+
1. **Reads settings** -- Loads `~/.claude/settings.json` (or `settings.local.json` if present).
|
|
20
|
+
2. **Backs up original** -- Saves a copy to `~/.clooks/settings.backup.json`.
|
|
21
|
+
3. **Extracts command hooks** -- Parses all command hooks from the settings file and creates corresponding handlers in `~/.clooks/manifest.yaml`.
|
|
22
|
+
4. **Rewrites settings** -- Replaces command hooks with HTTP hooks pointing to `http://localhost:7890`.
|
|
23
|
+
5. **Adds ensure-running** -- Injects a `clooks ensure-running` command into `SessionStart` so the daemon auto-starts when Claude Code launches.
|
|
24
|
+
6. **Imports plugin hooks** -- Detects and imports any Claude Code plugin hooks into the manifest.
|
|
25
|
+
7. **Installs system service** -- Sets up launchd (macOS) or systemd (Linux) for auto-start on login and crash recovery.
|
|
26
|
+
|
|
27
|
+
## Verify
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
clooks doctor
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
A passing report confirms that the daemon is running, the manifest is valid, and `settings.json` points to clooks.
|
|
34
|
+
|
|
35
|
+
## Rollback
|
|
36
|
+
|
|
37
|
+
If anything goes wrong, restore your original settings:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
clooks restore
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This replaces `settings.json` with the backup created during migration. Your command hooks return to their original state.
|
|
44
|
+
|
|
45
|
+
## Keeping settings in sync
|
|
46
|
+
|
|
47
|
+
After migration, if you add new handlers to the manifest, run:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
clooks sync
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This updates `settings.json` to include HTTP hook entries for any new events in your manifest. You do not need to edit `settings.json` manually.
|
|
54
|
+
|
|
55
|
+
> **Note:** `clooks sync` only adds missing entries. It never removes or modifies existing hooks in `settings.json`.
|
|
56
|
+
|
|
57
|
+
## Summary
|
|
58
|
+
|
|
59
|
+
| Command | What it does |
|
|
60
|
+
|---------|-------------|
|
|
61
|
+
| `clooks migrate` | Full migration: backup, convert, rewrite, install service |
|
|
62
|
+
| `clooks restore` | Rollback to pre-migration settings.json |
|
|
63
|
+
| `clooks sync` | Add missing HTTP hook entries for new manifest events |
|
|
64
|
+
| `clooks doctor` | Verify everything is wired up correctly |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
[Home](../index.md) | Prev: [Quickstart](quickstart.md) | Next: [Manifest Guide](../guides/manifest.md)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
Get your first clooks handler running in 5 minutes.
|
|
4
|
+
|
|
5
|
+
## 1. Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @mauribadnights/clooks
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 2. Initialize
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
clooks init
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 3. Start the daemon
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
clooks start
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 4. Check status
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
clooks status
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You should see the daemon running on port 7890 with zero handlers loaded.
|
|
30
|
+
|
|
31
|
+
## 5. Add a handler
|
|
32
|
+
|
|
33
|
+
Open `~/.clooks/manifest.yaml` in your editor and add a handler under `PreToolUse`:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
handlers:
|
|
37
|
+
PreToolUse:
|
|
38
|
+
- id: bash-logger
|
|
39
|
+
type: script
|
|
40
|
+
command: "echo '{\"additionalContext\": \"Reviewed by clooks\"}'"
|
|
41
|
+
filter: "Bash"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This handler fires before every Bash tool call and injects a note into Claude's context.
|
|
45
|
+
|
|
46
|
+
## 6. Hot-reload
|
|
47
|
+
|
|
48
|
+
Save the file. The daemon watches `manifest.yaml` and hot-reloads on change -- no restart needed.
|
|
49
|
+
|
|
50
|
+
You can confirm the reload in the daemon log:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
tail -1 ~/.clooks/daemon.log
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 7. Try it
|
|
57
|
+
|
|
58
|
+
Open Claude Code and run any Bash command. The handler fires automatically. You will see "Reviewed by clooks" appear in the context.
|
|
59
|
+
|
|
60
|
+
## 8. Check metrics
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
clooks stats
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This launches an interactive TUI showing execution counts, latency, and errors per event. Use `-t` for plain text output.
|
|
67
|
+
|
|
68
|
+
## What to try next
|
|
69
|
+
|
|
70
|
+
- Add a `filter` to scope handlers to specific tools
|
|
71
|
+
- Try an `llm` handler for AI-powered review
|
|
72
|
+
- Run `clooks migrate` to convert existing command hooks
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
[Home](../index.md) | Prev: [Installation](installation.md) | Next: [Migration](migration.md)
|
|
@@ -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)
|