claude_hooks 1.1.0 → 1.2.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.
- checksums.yaml +4 -4
- data/.agents/skills/ci-monitoring/SKILL.md +103 -0
- data/.agents/skills/run-tests/SKILL.md +45 -0
- data/AGENTS.md +46 -0
- data/CHANGELOG.md +64 -0
- data/README.md +303 -239
- data/docs/1.0.0_MIGRATION_GUIDE.md +6 -7
- data/docs/API/COMMON.md +7 -1
- data/docs/API/CONFIG_CHANGE.md +39 -0
- data/docs/API/CWD_CHANGED.md +34 -0
- data/docs/API/ELICITATION.md +44 -0
- data/docs/API/ELICITATION_RESULT.md +43 -0
- data/docs/API/FILE_CHANGED.md +37 -0
- data/docs/API/INSTRUCTIONS_LOADED.md +30 -0
- data/docs/API/MESSAGE_DISPLAY.md +38 -0
- data/docs/API/PERMISSION_DENIED.md +37 -0
- data/docs/API/PERMISSION_REQUEST.md +11 -8
- data/docs/API/POST_COMPACT.md +30 -0
- data/docs/API/POST_TOOL_BATCH.md +39 -0
- data/docs/API/POST_TOOL_USE_FAILURE.md +31 -0
- data/docs/API/PRE_COMPACT.md +10 -2
- data/docs/API/PRE_TOOL_USE.md +5 -1
- data/docs/API/SETUP.md +37 -0
- data/docs/API/STOP_FAILURE.md +19 -0
- data/docs/API/SUBAGENT_START.md +40 -0
- data/docs/API/TASK_COMPLETED.md +43 -0
- data/docs/API/TASK_CREATED.md +43 -0
- data/docs/API/TEAMMATE_IDLE.md +40 -0
- data/docs/API/USER_PROMPT_EXPANSION.md +43 -0
- data/docs/API/WORKTREE_CREATE.md +39 -0
- data/docs/API/WORKTREE_REMOVE.md +33 -0
- data/docs/PROMPT_BASED_HOOKS.md +386 -0
- data/docs/external/claude-hooks-reference.md +2406 -854
- data/docs/mitts/ideabox.md +12 -0
- data/docs/mitts/prd.md +417 -0
- data/docs/mitts/setup.md +303 -0
- data/docs/mitts/task.md +44 -0
- data/example_dotclaude/hooks/entrypoints/session_end.rb +5 -22
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit.rb +4 -26
- data/example_dotclaude/hooks/{handlers/pre_tool_use/github_guard.rb → github_guard.rb} +7 -13
- data/example_dotclaude/settings.json +1 -1
- data/lib/claude_hooks/base.rb +25 -0
- data/lib/claude_hooks/cli.rb +84 -149
- data/lib/claude_hooks/config_change.rb +28 -0
- data/lib/claude_hooks/cwd_changed.rb +28 -0
- data/lib/claude_hooks/elicitation.rb +61 -0
- data/lib/claude_hooks/elicitation_result.rb +57 -0
- data/lib/claude_hooks/file_changed.rb +41 -0
- data/lib/claude_hooks/instructions_loaded.rb +23 -0
- data/lib/claude_hooks/message_display.rb +58 -0
- data/lib/claude_hooks/output/base.rb +46 -1
- data/lib/claude_hooks/output/config_change.rb +47 -0
- data/lib/claude_hooks/output/cwd_changed.rb +42 -0
- data/lib/claude_hooks/output/elicitation.rb +67 -0
- data/lib/claude_hooks/output/elicitation_result.rb +63 -0
- data/lib/claude_hooks/output/file_changed.rb +42 -0
- data/lib/claude_hooks/output/instructions_loaded.rb +19 -0
- data/lib/claude_hooks/output/message_display.rb +49 -0
- data/lib/claude_hooks/output/permission_denied.rb +42 -0
- data/lib/claude_hooks/output/permission_request.rb +50 -25
- data/lib/claude_hooks/output/post_compact.rb +18 -0
- data/lib/claude_hooks/output/post_tool_batch.rb +51 -0
- data/lib/claude_hooks/output/post_tool_use.rb +12 -0
- data/lib/claude_hooks/output/post_tool_use_failure.rb +42 -0
- data/lib/claude_hooks/output/pre_compact.rb +34 -3
- data/lib/claude_hooks/output/pre_tool_use.rb +8 -2
- data/lib/claude_hooks/output/session_start.rb +30 -8
- data/lib/claude_hooks/output/setup.rb +42 -0
- data/lib/claude_hooks/output/stop.rb +15 -0
- data/lib/claude_hooks/output/stop_failure.rb +19 -0
- data/lib/claude_hooks/output/subagent_start.rb +42 -0
- data/lib/claude_hooks/output/task_completed.rb +18 -0
- data/lib/claude_hooks/output/task_created.rb +18 -0
- data/lib/claude_hooks/output/teammate_idle.rb +18 -0
- data/lib/claude_hooks/output/user_prompt_expansion.rb +51 -0
- data/lib/claude_hooks/output/worktree_create.rb +60 -0
- data/lib/claude_hooks/output/worktree_remove.rb +19 -0
- data/lib/claude_hooks/permission_denied.rb +41 -0
- data/lib/claude_hooks/permission_request.rb +16 -9
- data/lib/claude_hooks/post_compact.rb +23 -0
- data/lib/claude_hooks/post_tool_batch.rb +59 -0
- data/lib/claude_hooks/post_tool_use.rb +10 -0
- data/lib/claude_hooks/post_tool_use_failure.rb +45 -0
- data/lib/claude_hooks/pre_compact.rb +7 -0
- data/lib/claude_hooks/pre_tool_use.rb +20 -2
- data/lib/claude_hooks/session_start.rb +28 -0
- data/lib/claude_hooks/setup.rb +25 -0
- data/lib/claude_hooks/stop.rb +17 -0
- data/lib/claude_hooks/stop_failure.rb +27 -0
- data/lib/claude_hooks/subagent_start.rb +23 -0
- data/lib/claude_hooks/subagent_stop.rb +5 -1
- data/lib/claude_hooks/task_completed.rb +35 -0
- data/lib/claude_hooks/task_created.rb +35 -0
- data/lib/claude_hooks/teammate_idle.rb +23 -0
- data/lib/claude_hooks/user_prompt_expansion.rb +45 -0
- data/lib/claude_hooks/version.rb +1 -1
- data/lib/claude_hooks/worktree_create.rb +27 -0
- data/lib/claude_hooks/worktree_remove.rb +19 -0
- data/lib/claude_hooks.rb +40 -0
- metadata +71 -4
- data/example_dotclaude/hooks/entrypoints/pre_tool_use.rb +0 -25
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Ruby DSL for Claude Code hooks
|
|
2
2
|
|
|
3
3
|
> [!IMPORTANT]
|
|
4
|
-
> v1.
|
|
4
|
+
> v1.2.0 just released and is introducing a great number of changes. Please read the [CHANGELOG](CHANGELOG.md) for more information.
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
A Ruby DSL (Domain Specific Language) for creating Claude Code hooks. This will hopefully make creating and configuring new hooks way easier.
|
|
@@ -24,6 +24,7 @@ A Ruby DSL (Domain Specific Language) for creating Claude Code hooks. This will
|
|
|
24
24
|
- [📝 Example: Tool usage monitor](#-example-tool-usage-monitor)
|
|
25
25
|
- [🔄 Hook Output](#-hook-output)
|
|
26
26
|
- [🔌 Plugin Hooks Support](#-plugin-hooks-support)
|
|
27
|
+
- [🛠️ MCP Tools Integration](#️-mcp-tools-integration)
|
|
27
28
|
- [🚨 Advices](#-advices)
|
|
28
29
|
- [⚠️ Troubleshooting](#️-troubleshooting)
|
|
29
30
|
- [🧪 CLI Debugging](#-cli-debugging)
|
|
@@ -35,7 +36,11 @@ A Ruby DSL (Domain Specific Language) for creating Claude Code hooks. This will
|
|
|
35
36
|
> [!TIP]
|
|
36
37
|
> Examples are available in [`example_dotclaude/hooks/`](example_dotclaude/hooks/). The GithubGuard in particular is a good example of a solid hook. You can also check [Kyle's hooks for some great examples](https://github.com/kylesnowschwartz/dotfiles/blob/main/claude/hooks)
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
Claude Code supports two types of hooks:
|
|
40
|
+
- **Command hooks** (`type: "command"`) - Execute Ruby/bash scripts (what this DSL is for)
|
|
41
|
+
- **Prompt-based hooks** (`type: "prompt"`) - Delegate decisions to an LLM ([see guide](docs/PROMPT_BASED_HOOKS.md))
|
|
42
|
+
|
|
43
|
+
Here's how to create a simple command hook with this DSL:
|
|
39
44
|
|
|
40
45
|
1. **Install the gem:**
|
|
41
46
|
```bash
|
|
@@ -45,10 +50,8 @@ Here's how to create a simple hook:
|
|
|
45
50
|
1. **Create a simple hook script**
|
|
46
51
|
```ruby
|
|
47
52
|
#!/usr/bin/env ruby
|
|
48
|
-
require 'json'
|
|
49
53
|
require 'claude_hooks'
|
|
50
54
|
|
|
51
|
-
# Inherit from the right hook type class to get access to helper methods
|
|
52
55
|
class AddContextAfterPrompt < ClaudeHooks::UserPromptSubmit
|
|
53
56
|
def call
|
|
54
57
|
log "User asked: #{prompt}"
|
|
@@ -57,18 +60,7 @@ class AddContextAfterPrompt < ClaudeHooks::UserPromptSubmit
|
|
|
57
60
|
end
|
|
58
61
|
end
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
if __FILE__ == $0
|
|
62
|
-
# Read Claude Code's input data from STDIN
|
|
63
|
-
input_data = JSON.parse(STDIN.read)
|
|
64
|
-
|
|
65
|
-
hook = AddContextAfterPrompt.new(input_data)
|
|
66
|
-
hook.call
|
|
67
|
-
|
|
68
|
-
# Handles output and exit code depending on the hook state.
|
|
69
|
-
# In this case, uses exit code 0 (success) and prints output to STDOUT
|
|
70
|
-
hook.output_and_exit
|
|
71
|
-
end
|
|
63
|
+
ClaudeHooks::CLI.run_hook(AddContextAfterPrompt)
|
|
72
64
|
```
|
|
73
65
|
|
|
74
66
|
3. ⚠️ **Make it executable**
|
|
@@ -97,7 +89,7 @@ echo '{"session_id":"test","prompt":"Hello!"}' | ./add_context_after_prompt.rb
|
|
|
97
89
|
That's it! Your hook will now add context to every user prompt. 🎉
|
|
98
90
|
|
|
99
91
|
> [!TIP]
|
|
100
|
-
>
|
|
92
|
+
> Need to run multiple hooks for the same event and merge their outputs? See [Multi-hook structure](#multi-hook-structure) below.
|
|
101
93
|
|
|
102
94
|
## 📦 Installation
|
|
103
95
|
|
|
@@ -227,50 +219,83 @@ end
|
|
|
227
219
|
### Core Components
|
|
228
220
|
|
|
229
221
|
1. **`ClaudeHooks::Base`** - Base class with common functionality (logging, config, validation)
|
|
230
|
-
2. **Hook
|
|
231
|
-
3. **Output Classes
|
|
232
|
-
4.
|
|
233
|
-
5. **
|
|
222
|
+
2. **Hook Classes** - One class per event type (`ClaudeHooks::UserPromptSubmit`, `ClaudeHooks::PreToolUse`, etc.) that you can inherit from in your hook scripts
|
|
223
|
+
3. **Output Classes**: those hook classes return instances of output objects (`ClaudeHooks::Output::UserPromptSubmit`, etc.) that handle intelligent merging of multiple outputs, correct exit codes, and routing to `STDOUT` or `STDERR` depending on hook state
|
|
224
|
+
4. **`ClaudeHooks::CLI`** - Entrypoint helpers: `CLI.run_hook` for production, `CLI.test_runner`/`CLI.run_with_sample_data` for local testing
|
|
225
|
+
5. **Configuration** - Shared configuration management via `ClaudeHooks::Configuration`
|
|
226
|
+
6. **Logger** - Dedicated logging class with multiline block support
|
|
227
|
+
|
|
228
|
+
### Hook file structure
|
|
229
|
+
|
|
230
|
+
For simple cases like one hook class per event, a single file is all you need. Name each file after what it does — the event it runs on comes from where you register it in `settings.json`, not from the filename:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
.claude/hooks/
|
|
234
|
+
├── github_guard.rb # PreToolUse — ClaudeHooks::CLI.run_hook(GithubGuard, on_error: :block)
|
|
235
|
+
├── format_on_write.rb # PostToolUse — ClaudeHooks::CLI.run_hook(FormatOnWrite)
|
|
236
|
+
├── load_project_rules.rb # SessionStart — ClaudeHooks::CLI.run_hook(LoadProjectRules)
|
|
237
|
+
└── append_rules.rb # UserPromptSubmit — ClaudeHooks::CLI.run_hook(AppendRules)
|
|
238
|
+
```
|
|
234
239
|
|
|
235
|
-
|
|
240
|
+
See [`example_dotclaude/hooks/github_guard.rb`](example_dotclaude/hooks/github_guard.rb) for a complete, self-contained `PreToolUse` hook wired up this way.
|
|
241
|
+
|
|
242
|
+
### Multi-hook structure
|
|
243
|
+
|
|
244
|
+
When you need to run multiple hooks for the same event and merge their outputs, split into entrypoints and handlers:
|
|
236
245
|
|
|
237
246
|
```
|
|
238
247
|
.claude/hooks/
|
|
239
|
-
├── entrypoints/
|
|
240
|
-
│
|
|
241
|
-
│
|
|
242
|
-
│
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
│
|
|
246
|
-
│
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
│ ├── append_rules.rb
|
|
251
|
-
│ └── log_user_prompt.rb
|
|
252
|
-
├── pre_tool_use/
|
|
253
|
-
│ ├── github_guard.rb
|
|
254
|
-
│ └── tool_monitor.rb
|
|
255
|
-
└── ...
|
|
248
|
+
├── entrypoints/ # Coordinates multiple handlers per event
|
|
249
|
+
│ ├── session_end.rb
|
|
250
|
+
│ └── user_prompt_submit.rb
|
|
251
|
+
│
|
|
252
|
+
└── handlers/ # One class per concern
|
|
253
|
+
├── session_end/
|
|
254
|
+
│ ├── cleanup_handler.rb
|
|
255
|
+
│ └── log_session_stats.rb
|
|
256
|
+
└── user_prompt_submit/
|
|
257
|
+
├── append_rules.rb
|
|
258
|
+
└── log_user_prompt.rb
|
|
256
259
|
```
|
|
257
260
|
|
|
261
|
+
Use this structure only when you need `Output.merge` across multiple handlers — a single-handler entrypoint is just noise; register the hook class directly instead (see [Hook file structure](#hook-file-structure) above). See [`example_dotclaude/hooks/entrypoints/session_end.rb`](example_dotclaude/hooks/entrypoints/session_end.rb) for a working two-handler entrypoint.
|
|
262
|
+
|
|
258
263
|
## 🪝 Hook Types
|
|
259
264
|
|
|
260
265
|
The framework supports the following hook types:
|
|
261
266
|
|
|
262
267
|
| Hook Type | Class | Description |
|
|
263
268
|
|-----------|-------|-------------|
|
|
264
|
-
| **[SessionStart](docs/API/SESSION_START.md)** | `ClaudeHooks::SessionStart` |
|
|
265
|
-
| **[
|
|
266
|
-
| **[
|
|
267
|
-
| **[
|
|
268
|
-
| **[
|
|
269
|
-
| **[
|
|
270
|
-
| **[
|
|
271
|
-
| **[
|
|
272
|
-
| **[
|
|
273
|
-
| **[
|
|
269
|
+
| **[SessionStart](docs/API/SESSION_START.md)** | `ClaudeHooks::SessionStart` | Runs when Claude Code starts, resumes, or compacts a session |
|
|
270
|
+
| **[Setup](docs/API/SETUP.md)** | `ClaudeHooks::Setup` | Runs once on Claude Code startup before any session |
|
|
271
|
+
| **[UserPromptSubmit](docs/API/USER_PROMPT_SUBMIT.md)** | `ClaudeHooks::UserPromptSubmit` | Runs before the user's prompt is processed |
|
|
272
|
+
| **[UserPromptExpansion](docs/API/USER_PROMPT_EXPANSION.md)** | `ClaudeHooks::UserPromptExpansion` | Runs when a slash command or prompt expansion is triggered |
|
|
273
|
+
| **[Notification](docs/API/NOTIFICATION.md)** | `ClaudeHooks::Notification` | Runs when Claude Code sends a notification |
|
|
274
|
+
| **[MessageDisplay](docs/API/MESSAGE_DISPLAY.md)** | `ClaudeHooks::MessageDisplay` | Runs when a message is about to be displayed |
|
|
275
|
+
| **[PreToolUse](docs/API/PRE_TOOL_USE.md)** | `ClaudeHooks::PreToolUse` | Runs before a tool is used; can allow, deny, defer or ask |
|
|
276
|
+
| **[PermissionRequest](docs/API/PERMISSION_REQUEST.md)** | `ClaudeHooks::PermissionRequest` | Runs when Claude requests an explicit permission |
|
|
277
|
+
| **[PermissionDenied](docs/API/PERMISSION_DENIED.md)** | `ClaudeHooks::PermissionDenied` | Runs when a permission request is denied; can request a retry |
|
|
278
|
+
| **[PostToolUse](docs/API/POST_TOOL_USE.md)** | `ClaudeHooks::PostToolUse` | Runs after a tool is used; can rewrite output |
|
|
279
|
+
| **[PostToolBatch](docs/API/POST_TOOL_BATCH.md)** | `ClaudeHooks::PostToolBatch` | Runs after a full batch of tool calls completes |
|
|
280
|
+
| **[PostToolUseFailure](docs/API/POST_TOOL_USE_FAILURE.md)** | `ClaudeHooks::PostToolUseFailure` | Runs when a tool call fails |
|
|
281
|
+
| **[Stop](docs/API/STOP.md)** | `ClaudeHooks::Stop` | Runs when Claude Code finishes responding; can force continuation |
|
|
282
|
+
| **[StopFailure](docs/API/STOP_FAILURE.md)** | `ClaudeHooks::StopFailure` | Runs when the stop phase itself errors; logging only |
|
|
283
|
+
| **[SubagentStart](docs/API/SUBAGENT_START.md)** | `ClaudeHooks::SubagentStart` | Runs when a subagent task starts |
|
|
284
|
+
| **[SubagentStop](docs/API/SUBAGENT_STOP.md)** | `ClaudeHooks::SubagentStop` | Runs when a subagent task completes |
|
|
285
|
+
| **[TaskCreated](docs/API/TASK_CREATED.md)** | `ClaudeHooks::TaskCreated` | Runs when a teammate task is created |
|
|
286
|
+
| **[TaskCompleted](docs/API/TASK_COMPLETED.md)** | `ClaudeHooks::TaskCompleted` | Runs when a teammate task completes |
|
|
287
|
+
| **[TeammateIdle](docs/API/TEAMMATE_IDLE.md)** | `ClaudeHooks::TeammateIdle` | Runs when a teammate goes idle |
|
|
288
|
+
| **[PreCompact](docs/API/PRE_COMPACT.md)** | `ClaudeHooks::PreCompact` | Runs before transcript compaction; can block it |
|
|
289
|
+
| **[PostCompact](docs/API/POST_COMPACT.md)** | `ClaudeHooks::PostCompact` | Runs after transcript compaction completes |
|
|
290
|
+
| **[ConfigChange](docs/API/CONFIG_CHANGE.md)** | `ClaudeHooks::ConfigChange` | Runs when Claude Code configuration changes; can block it |
|
|
291
|
+
| **[CwdChanged](docs/API/CWD_CHANGED.md)** | `ClaudeHooks::CwdChanged` | Runs when the working directory changes |
|
|
292
|
+
| **[FileChanged](docs/API/FILE_CHANGED.md)** | `ClaudeHooks::FileChanged` | Runs when a watched file is created, modified, or deleted |
|
|
293
|
+
| **[InstructionsLoaded](docs/API/INSTRUCTIONS_LOADED.md)** | `ClaudeHooks::InstructionsLoaded` | Runs when a CLAUDE.md instructions file is loaded |
|
|
294
|
+
| **[Elicitation](docs/API/ELICITATION.md)** | `ClaudeHooks::Elicitation` | Runs when an MCP server requests user input |
|
|
295
|
+
| **[ElicitationResult](docs/API/ELICITATION_RESULT.md)** | `ClaudeHooks::ElicitationResult` | Runs after an elicitation response is provided |
|
|
296
|
+
| **[WorktreeCreate](docs/API/WORKTREE_CREATE.md)** | `ClaudeHooks::WorktreeCreate` | Runs when Claude Code creates a git worktree |
|
|
297
|
+
| **[WorktreeRemove](docs/API/WORKTREE_REMOVE.md)** | `ClaudeHooks::WorktreeRemove` | Runs when a git worktree is removed |
|
|
298
|
+
| **[SessionEnd](docs/API/SESSION_END.md)** | `ClaudeHooks::SessionEnd` | Runs when a Claude Code session ends |
|
|
274
299
|
|
|
275
300
|
## 🚀 Claude Hook Flow
|
|
276
301
|
|
|
@@ -289,37 +314,11 @@ graph LR
|
|
|
289
314
|
A[Hook triggers] --> B[JSON from STDIN] --> C[Hook does its thing] --> D[JSON to STDOUT or STDERR<br />Exit Code] --> E[Yields back to Claude Code] --> A
|
|
290
315
|
```
|
|
291
316
|
|
|
292
|
-
The main issue is that there are many different types of hooks and they each have different expectations regarding the data outputted to `
|
|
293
|
-
|
|
294
|
-
### 🔄 Proposal: a more robust Claude Hook execution flow
|
|
295
|
-
|
|
296
|
-
1. An entrypoint for a hook is set in `~/.claude/settings.json`
|
|
297
|
-
2. Claude Code calls the entrypoint script (e.g., `hooks/entrypoints/pre_tool_use.rb`)
|
|
298
|
-
3. The entrypoint script reads STDIN and coordinates multiple **hook handlers**
|
|
299
|
-
4. Each **hook handler** executes and returns its output data
|
|
300
|
-
5. The entrypoint script combines/processes outputs from multiple **hook handlers**
|
|
301
|
-
6. And then returns final response to Claude Code with the correct exit code
|
|
302
|
-
|
|
303
|
-
```mermaid
|
|
304
|
-
graph TD
|
|
305
|
-
A[🔧 Hook Configuration<br/>settings.json] --> B
|
|
306
|
-
B[🤖 Claude Code<br/><em>User submits prompt</em>] --> C[📋 Entrypoint<br />entrypoints/user_prompt_submit.rb]
|
|
307
|
-
|
|
308
|
-
C --> D[📋 Entrypoint<br />Parses JSON from STDIN]
|
|
309
|
-
D --> E[📋 Entrypoint<br />Calls hook handlers]
|
|
310
|
-
|
|
311
|
-
E --> F[📝 Handler<br />AppendContextRules.call<br/><em>Returns output</em>]
|
|
312
|
-
E --> G[📝 Handler<br />PromptGuard.call<br/><em>Returns output</em>]
|
|
313
|
-
|
|
314
|
-
F --> J[📋 Entrypoint<br />Calls _ClaudeHooks::Output::UserPromptSubmit.merge_ to 🔀 merge outputs]
|
|
315
|
-
G --> J
|
|
317
|
+
The main issue is that there are many different types of hooks and they each have different expectations regarding the data outputted to `STDOUT` or `STDERR` and Claude Code will react differently for each specific exit code used depending on the hook type. This DSL handles all of that for you.
|
|
316
318
|
|
|
317
|
-
|
|
318
|
-
K --> L[🤖 Yields back to Claude Code]
|
|
319
|
-
L --> B
|
|
320
|
-
```
|
|
319
|
+
### Basic hook structure
|
|
321
320
|
|
|
322
|
-
|
|
321
|
+
The simplest pattern is a single file: define your hook class, call `CLI.run_hook`. It handles STDIN parsing, error handling, and correct exit codes.
|
|
323
322
|
|
|
324
323
|
```ruby
|
|
325
324
|
#!/usr/bin/env ruby
|
|
@@ -328,47 +327,53 @@ require 'claude_hooks'
|
|
|
328
327
|
|
|
329
328
|
class AddContextAfterPrompt < ClaudeHooks::UserPromptSubmit
|
|
330
329
|
def call
|
|
331
|
-
#
|
|
332
|
-
log do
|
|
333
|
-
"--- INPUT DATA ---"
|
|
334
|
-
"session_id: #{session_id}"
|
|
335
|
-
"cwd: #{cwd}"
|
|
336
|
-
"hook_event_name: #{hook_event_name}"
|
|
337
|
-
"prompt: #{current_prompt}"
|
|
338
|
-
"---"
|
|
339
|
-
end
|
|
340
|
-
|
|
330
|
+
log "session_id: #{session_id}, prompt: #{prompt}"
|
|
341
331
|
log "Full conversation transcript: #{read_transcript}"
|
|
342
332
|
|
|
343
|
-
# Use a Hook state method to modify what's sent back to Claude Code
|
|
344
333
|
add_additional_context!("Some custom context")
|
|
345
334
|
|
|
346
|
-
|
|
347
|
-
if current_prompt.include?("bad word")
|
|
335
|
+
if prompt.include?("bad word")
|
|
348
336
|
block_prompt!("Hmm no no no!")
|
|
349
|
-
log "Prompt blocked: #{current_prompt} because of bad word"
|
|
350
337
|
end
|
|
351
338
|
|
|
352
|
-
# Return output if you need it
|
|
353
339
|
output
|
|
354
340
|
end
|
|
355
341
|
end
|
|
356
342
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
# Read Claude Code's input data from STDIN
|
|
360
|
-
input_data = JSON.parse(STDIN.read)
|
|
343
|
+
ClaudeHooks::CLI.run_hook(AddContextAfterPrompt)
|
|
344
|
+
```
|
|
361
345
|
|
|
362
|
-
|
|
363
|
-
# Call the hook
|
|
364
|
-
hook.call
|
|
346
|
+
### Multi-handler flow
|
|
365
347
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
348
|
+
When multiple hook classes need to respond to the same event, use an entrypoint file to coordinate them:
|
|
349
|
+
|
|
350
|
+
1. A hook is registered in `~/.claude/settings.json`
|
|
351
|
+
2. Claude Code calls an entrypoint script
|
|
352
|
+
3. The entrypoint instantiates each handler and calls them
|
|
353
|
+
4. Outputs are merged with `Output.merge` (most restrictive behavior wins)
|
|
354
|
+
5. The merged output is returned to Claude Code with the correct exit code
|
|
355
|
+
|
|
356
|
+
```mermaid
|
|
357
|
+
graph TD
|
|
358
|
+
A[🔧 Hook Configuration<br/>settings.json] --> B
|
|
359
|
+
B[🤖 Claude Code<br/><em>User submits prompt</em>] --> C[📋 Entrypoint<br />entrypoints/user_prompt_submit.rb]
|
|
360
|
+
|
|
361
|
+
C --> D[📋 Entrypoint<br />Parses JSON from STDIN]
|
|
362
|
+
D --> E[📋 Entrypoint<br />Calls hook handlers]
|
|
363
|
+
|
|
364
|
+
E --> F[📝 Handler<br />AppendRules.call<br/><em>Returns output</em>]
|
|
365
|
+
E --> G[📝 Handler<br />LogUserPrompt.call<br/><em>Returns output</em>]
|
|
366
|
+
|
|
367
|
+
F --> J[📋 Entrypoint<br />Calls _ClaudeHooks::Output::UserPromptSubmit.merge_ to 🔀 merge outputs]
|
|
368
|
+
G --> J
|
|
369
|
+
|
|
370
|
+
J --> K[📋 Entrypoint<br />- Writes output to STDOUT or STDERR<br />- Uses correct exit code]
|
|
371
|
+
K --> L[🤖 Yields back to Claude Code]
|
|
372
|
+
L --> B
|
|
370
373
|
```
|
|
371
374
|
|
|
375
|
+
See [Hook Output Merging](#-hook-output-merging) below for the entrypoint code that implements this flow, and [`example_dotclaude/hooks/entrypoints/user_prompt_submit.rb`](example_dotclaude/hooks/entrypoints/user_prompt_submit.rb) for a working `AppendRules` + `LogUserPrompt` example.
|
|
376
|
+
|
|
372
377
|
## 📚 API Reference
|
|
373
378
|
|
|
374
379
|
The goal of those APIs is to simplify reading from `STDIN` and writing to `STDOUT` or `STDERR` as well as exiting with the right exit codes: the way Claude Code expects you to.
|
|
@@ -388,16 +393,36 @@ The framework supports all existing hook types with their respective input field
|
|
|
388
393
|
|
|
389
394
|
| Hook Type | Input Fields |
|
|
390
395
|
|-----------|--------------|
|
|
391
|
-
| **Common** | `session_id`, `transcript_path`, `cwd`, `hook_event_name`, `permission_mode` |
|
|
396
|
+
| **Common (all hooks)** | `session_id`, `transcript_path`, `cwd`, `hook_event_name`, `permission_mode`, `prompt_id`, `agent_id`, `agent_type`, `effort` |
|
|
397
|
+
| **SessionStart** | `source`, `model`, `session_title` |
|
|
398
|
+
| **Setup** | `source` |
|
|
392
399
|
| **UserPromptSubmit** | `prompt` |
|
|
400
|
+
| **UserPromptExpansion** | `expansion_type`, `command_name`, `command_args`, `command_source`, `prompt` |
|
|
401
|
+
| **Notification** | `message`, `notification_type` |
|
|
402
|
+
| **MessageDisplay** | `turn_id`, `message_id`, `index`, `final`, `delta` |
|
|
393
403
|
| **PreToolUse** | `tool_name`, `tool_input`, `tool_use_id` |
|
|
394
|
-
| **PermissionRequest** | `tool_name`, `tool_input`, `tool_use_id` |
|
|
404
|
+
| **PermissionRequest** | `tool_name`, `tool_input`, `tool_use_id`, `permission_suggestions` |
|
|
405
|
+
| **PermissionDenied** | `tool_name`, `tool_input`, `tool_use_id`, `reason` |
|
|
395
406
|
| **PostToolUse** | `tool_name`, `tool_input`, `tool_response`, `tool_use_id` |
|
|
396
|
-
| **
|
|
397
|
-
| **
|
|
398
|
-
| **
|
|
407
|
+
| **PostToolBatch** | `tool_calls` |
|
|
408
|
+
| **PostToolUseFailure** | `tool_name`, `tool_input`, `tool_use_id`, `error`, `is_interrupt`, `duration_ms` |
|
|
409
|
+
| **Stop** | `stop_hook_active`, `last_assistant_message`, `background_tasks`, `session_crons` |
|
|
410
|
+
| **StopFailure** | `error`, `error_details`, `last_assistant_message` |
|
|
411
|
+
| **SubagentStart** | *(common only: `agent_id`, `agent_type`)* |
|
|
412
|
+
| **SubagentStop** | `stop_hook_active`, `agent_transcript_path` + common `agent_id`/`agent_type` |
|
|
413
|
+
| **TaskCreated** | `task_id`, `task_subject`, `task_description`, `teammate_name`, `team_name` |
|
|
414
|
+
| **TaskCompleted** | `task_id`, `task_subject`, `task_description`, `teammate_name`, `team_name` |
|
|
415
|
+
| **TeammateIdle** | `teammate_name`, `team_name` |
|
|
399
416
|
| **PreCompact** | `trigger`, `custom_instructions` |
|
|
400
|
-
| **
|
|
417
|
+
| **PostCompact** | `trigger`, `compact_summary` |
|
|
418
|
+
| **ConfigChange** | `source`, `file_path` |
|
|
419
|
+
| **CwdChanged** | `old_cwd`, `new_cwd` |
|
|
420
|
+
| **FileChanged** | `file_path`, `event` |
|
|
421
|
+
| **InstructionsLoaded** | `file_path`, `load_reason` |
|
|
422
|
+
| **Elicitation** | `mcp_server_name`, `message`, `mode`, `url`, `elicitation_id`, `requested_schema` |
|
|
423
|
+
| **ElicitationResult** | `mcp_server_name`, `action`, `mode`, `elicitation_id`, `content` |
|
|
424
|
+
| **WorktreeCreate** | `name` |
|
|
425
|
+
| **WorktreeRemove** | `worktree_path` |
|
|
401
426
|
| **SessionEnd** | `reason` |
|
|
402
427
|
|
|
403
428
|
### Hooks API
|
|
@@ -405,15 +430,35 @@ The framework supports all existing hook types with their respective input field
|
|
|
405
430
|
**All hook types** inherit from `ClaudeHooks::Base` and share a common API, as well as hook specific APIs.
|
|
406
431
|
|
|
407
432
|
- [📚 Common API Methods](docs/API/COMMON.md)
|
|
408
|
-
- [🔔 Notification Hooks](docs/API/NOTIFICATION.md)
|
|
409
433
|
- [🚀 Session Start Hooks](docs/API/SESSION_START.md)
|
|
434
|
+
- [⚙️ Setup Hooks](docs/API/SETUP.md)
|
|
410
435
|
- [🖋️ User Prompt Submit Hooks](docs/API/USER_PROMPT_SUBMIT.md)
|
|
436
|
+
- [🔀 User Prompt Expansion Hooks](docs/API/USER_PROMPT_EXPANSION.md)
|
|
437
|
+
- [🔔 Notification Hooks](docs/API/NOTIFICATION.md)
|
|
438
|
+
- [💬 Message Display Hooks](docs/API/MESSAGE_DISPLAY.md)
|
|
411
439
|
- [🛠️ Pre-Tool Use Hooks](docs/API/PRE_TOOL_USE.md)
|
|
412
440
|
- [🔐 Permission Request Hooks](docs/API/PERMISSION_REQUEST.md)
|
|
441
|
+
- [🚫 Permission Denied Hooks](docs/API/PERMISSION_DENIED.md)
|
|
413
442
|
- [🔧 Post-Tool Use Hooks](docs/API/POST_TOOL_USE.md)
|
|
414
|
-
- [
|
|
443
|
+
- [📦 Post-Tool Batch Hooks](docs/API/POST_TOOL_BATCH.md)
|
|
444
|
+
- [❌ Post-Tool Use Failure Hooks](docs/API/POST_TOOL_USE_FAILURE.md)
|
|
415
445
|
- [⏹️ Stop Hooks](docs/API/STOP.md)
|
|
446
|
+
- [💥 Stop Failure Hooks](docs/API/STOP_FAILURE.md)
|
|
447
|
+
- [▶️ Subagent Start Hooks](docs/API/SUBAGENT_START.md)
|
|
416
448
|
- [⏹️ Subagent Stop Hooks](docs/API/SUBAGENT_STOP.md)
|
|
449
|
+
- [✅ Task Created Hooks](docs/API/TASK_CREATED.md)
|
|
450
|
+
- [✅ Task Completed Hooks](docs/API/TASK_COMPLETED.md)
|
|
451
|
+
- [💤 Teammate Idle Hooks](docs/API/TEAMMATE_IDLE.md)
|
|
452
|
+
- [📝 Pre-Compact Hooks](docs/API/PRE_COMPACT.md)
|
|
453
|
+
- [📄 Post-Compact Hooks](docs/API/POST_COMPACT.md)
|
|
454
|
+
- [🔩 Config Change Hooks](docs/API/CONFIG_CHANGE.md)
|
|
455
|
+
- [📂 Cwd Changed Hooks](docs/API/CWD_CHANGED.md)
|
|
456
|
+
- [📄 File Changed Hooks](docs/API/FILE_CHANGED.md)
|
|
457
|
+
- [📋 Instructions Loaded Hooks](docs/API/INSTRUCTIONS_LOADED.md)
|
|
458
|
+
- [💬 Elicitation Hooks](docs/API/ELICITATION.md)
|
|
459
|
+
- [💬 Elicitation Result Hooks](docs/API/ELICITATION_RESULT.md)
|
|
460
|
+
- [🌳 Worktree Create Hooks](docs/API/WORKTREE_CREATE.md)
|
|
461
|
+
- [🗑️ Worktree Remove Hooks](docs/API/WORKTREE_REMOVE.md)
|
|
417
462
|
- [🔚 Session End Hooks](docs/API/SESSION_END.md)
|
|
418
463
|
|
|
419
464
|
### 📝 Logging
|
|
@@ -457,7 +502,7 @@ Logs are written to session-specific files in the configured log directory:
|
|
|
457
502
|
|
|
458
503
|
Let's create a hook that will monitor tool usage and ask for permission before using dangerous tools.
|
|
459
504
|
|
|
460
|
-
First, register
|
|
505
|
+
First, register your hook in `~/.claude/settings.json`:
|
|
461
506
|
|
|
462
507
|
```json
|
|
463
508
|
"hooks": {
|
|
@@ -467,7 +512,7 @@ First, register an entrypoint in `~/.claude/settings.json`:
|
|
|
467
512
|
"hooks": [
|
|
468
513
|
{
|
|
469
514
|
"type": "command",
|
|
470
|
-
"command": "~/.claude/hooks/
|
|
515
|
+
"command": "~/.claude/hooks/tool_monitor.rb"
|
|
471
516
|
}
|
|
472
517
|
]
|
|
473
518
|
}
|
|
@@ -475,43 +520,11 @@ First, register an entrypoint in `~/.claude/settings.json`:
|
|
|
475
520
|
}
|
|
476
521
|
```
|
|
477
522
|
|
|
478
|
-
Then
|
|
479
|
-
```bash
|
|
480
|
-
touch ~/.claude/hooks/entrypoints/pre_tool_use.rb
|
|
481
|
-
chmod +x ~/.claude/hooks/entrypoints/pre_tool_use.rb
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
```ruby
|
|
485
|
-
#!/usr/bin/env ruby
|
|
486
|
-
|
|
487
|
-
require 'json'
|
|
488
|
-
require_relative '../handlers/pre_tool_use/tool_monitor'
|
|
489
|
-
|
|
490
|
-
begin
|
|
491
|
-
# Read input from stdin
|
|
492
|
-
input_data = JSON.parse(STDIN.read)
|
|
493
|
-
|
|
494
|
-
tool_monitor = ToolMonitor.new(input_data)
|
|
495
|
-
tool_monitor.call
|
|
496
|
-
|
|
497
|
-
# You could also call any other handler here and then merge the outputs
|
|
498
|
-
|
|
499
|
-
tool_monitor.output_and_exit
|
|
500
|
-
rescue StandardError => e
|
|
501
|
-
STDERR.puts JSON.generate({
|
|
502
|
-
continue: false,
|
|
503
|
-
stopReason: "Hook execution error: #{e.message}",
|
|
504
|
-
suppressOutput: false
|
|
505
|
-
})
|
|
506
|
-
# Non-blocking error
|
|
507
|
-
exit 1
|
|
508
|
-
end
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
Finally, create the handler that will be used to monitor tool usage.
|
|
523
|
+
Then create the hook script and make it executable:
|
|
512
524
|
|
|
513
525
|
```bash
|
|
514
|
-
touch ~/.claude/hooks/
|
|
526
|
+
touch ~/.claude/hooks/tool_monitor.rb
|
|
527
|
+
chmod +x ~/.claude/hooks/tool_monitor.rb
|
|
515
528
|
```
|
|
516
529
|
|
|
517
530
|
```ruby
|
|
@@ -527,17 +540,16 @@ class ToolMonitor < ClaudeHooks::PreToolUse
|
|
|
527
540
|
|
|
528
541
|
if DANGEROUS_TOOLS.include?(tool_name)
|
|
529
542
|
log "Dangerous tool detected: #{tool_name}", level: :warn
|
|
530
|
-
# Use one of the ClaudeHooks::PreToolUse methods to modify the hook state and block the tool
|
|
531
543
|
ask_for_permission!("The tool '#{tool_name}' can impact your system. Allow?")
|
|
532
544
|
else
|
|
533
|
-
# Use one of the ClaudeHooks::PreToolUse methods to modify the hook state and allow the tool
|
|
534
545
|
approve_tool!("Safe tool usage")
|
|
535
546
|
end
|
|
536
547
|
|
|
537
|
-
# Accessor provided by ClaudeHooks::PreToolUse
|
|
538
548
|
output
|
|
539
549
|
end
|
|
540
550
|
end
|
|
551
|
+
|
|
552
|
+
ClaudeHooks::CLI.run_hook(ToolMonitor, on_error: :block)
|
|
541
553
|
```
|
|
542
554
|
|
|
543
555
|
## 🔄 Hook Output
|
|
@@ -555,24 +567,24 @@ This method will return an output object based on the hook's type class (e.g: `C
|
|
|
555
567
|
|
|
556
568
|
### 🔄 Hook Output Merging
|
|
557
569
|
|
|
558
|
-
|
|
559
|
-
Each hook type's `output` provides a `merge` method that will try to intelligently merge multiple hook results.
|
|
560
|
-
Merged outputs always inherit the **most restrictive behavior**.
|
|
570
|
+
When running multiple hooks for the same event, each hook type's `output` provides a `merge` method that intelligently combines results. Merged outputs always inherit the **most restrictive behavior**.
|
|
561
571
|
|
|
562
572
|
```ruby
|
|
573
|
+
#!/usr/bin/env ruby
|
|
563
574
|
|
|
564
575
|
require 'json'
|
|
576
|
+
require 'claude_hooks'
|
|
565
577
|
require_relative '../handlers/user_prompt_submit/hook1'
|
|
566
578
|
require_relative '../handlers/user_prompt_submit/hook2'
|
|
567
579
|
require_relative '../handlers/user_prompt_submit/hook3'
|
|
568
580
|
|
|
569
581
|
begin
|
|
570
|
-
# Read input from stdin
|
|
582
|
+
# Read input from stdin
|
|
571
583
|
input_data = JSON.parse(STDIN.read)
|
|
572
584
|
|
|
573
585
|
hook1 = Hook1.new(input_data)
|
|
574
|
-
hook2 =
|
|
575
|
-
hook3 =
|
|
586
|
+
hook2 = Hook2.new(input_data)
|
|
587
|
+
hook3 = Hook3.new(input_data)
|
|
576
588
|
|
|
577
589
|
# Execute the multiple hooks
|
|
578
590
|
hook1.call
|
|
@@ -594,6 +606,14 @@ begin
|
|
|
594
606
|
|
|
595
607
|
# Automatically handles outputting to the right stream (STDOUT or STDERR) and uses the right exit code depending on hook state
|
|
596
608
|
merged_output.output_and_exit
|
|
609
|
+
rescue StandardError => e
|
|
610
|
+
# This is exactly what CLI.run_hook does for you (non-blocking / fail-open):
|
|
611
|
+
STDERR.puts JSON.generate({
|
|
612
|
+
continue: false,
|
|
613
|
+
stopReason: "Hook execution error: #{e.message}",
|
|
614
|
+
suppressOutput: false
|
|
615
|
+
})
|
|
616
|
+
exit 1
|
|
597
617
|
end
|
|
598
618
|
```
|
|
599
619
|
|
|
@@ -625,6 +645,13 @@ Claude Code hooks support multiple exit codes with different behaviors depending
|
|
|
625
645
|
| SessionStart | Operation continues<br/><br />**`STDOUT` added as context to Claude** | Non-blocking error<br/><br />`STDERR` shown to user | N/A<br/><br />`STDERR` shown to user only |
|
|
626
646
|
| SessionEnd | Operation continues<br/><br />Logged to debug only (`--debug`) | Non-blocking error<br/><br />Logged to debug only (`--debug`) | N/A<br/><br />Logged to debug only (`--debug`) |
|
|
627
647
|
|
|
648
|
+
> [!NOTE]
|
|
649
|
+
> The 20 events added in `1.2.0` follow the same families. Their per-event exit-code behavior is documented on each API page under [`docs/API/`](docs/API/):
|
|
650
|
+
> - **Blocking via top-level `decision`** (behave like `PreToolUse`/`Stop`): `UserPromptExpansion`, `PostToolBatch`, `ConfigChange`.
|
|
651
|
+
> - **Blocking via `exit 2` / `continue: false`** (no `decision` field): `TaskCreated`, `TaskCompleted`, `TeammateIdle`.
|
|
652
|
+
> - **JSON-API special** (always `exit 0`, decision in `hookSpecificOutput`): `PermissionDenied`, `Elicitation`, `ElicitationResult`, `WorktreeCreate` (bare-path stdout).
|
|
653
|
+
> - **Non-blocking / context-only** (exit code effectively ignored): `Setup`, `SubagentStart`, `PostToolUseFailure`, `StopFailure`, `PostCompact`, `CwdChanged`, `FileChanged`, `InstructionsLoaded`, `WorktreeRemove`, `MessageDisplay`.
|
|
654
|
+
|
|
628
655
|
|
|
629
656
|
#### Manually outputing and exiting example with success
|
|
630
657
|
For the operation to continue for a `UserPromptSubmit` hook, you would `STDOUT.puts` structured JSON data followed by `exit 0`:
|
|
@@ -709,12 +736,7 @@ class PluginFormatter < ClaudeHooks::PostToolUse
|
|
|
709
736
|
end
|
|
710
737
|
end
|
|
711
738
|
|
|
712
|
-
|
|
713
|
-
input_data = JSON.parse(STDIN.read)
|
|
714
|
-
hook = PluginFormatter.new(input_data)
|
|
715
|
-
hook.call
|
|
716
|
-
hook.output_and_exit
|
|
717
|
-
end
|
|
739
|
+
ClaudeHooks::CLI.run_hook(PluginFormatter)
|
|
718
740
|
```
|
|
719
741
|
|
|
720
742
|
**Environment variables available in plugins:**
|
|
@@ -722,7 +744,70 @@ end
|
|
|
722
744
|
- `${CLAUDE_PROJECT_DIR}`: Project root directory (same as for project hooks)
|
|
723
745
|
- All standard environment variables and configuration options work the same way
|
|
724
746
|
|
|
725
|
-
See the [plugin components reference](https://
|
|
747
|
+
See the [plugin components reference](https://code.claude.com/docs/en/plugins-reference#hooks) for more details on creating plugin hooks.
|
|
748
|
+
|
|
749
|
+
## 🛠️ MCP Tools Integration
|
|
750
|
+
|
|
751
|
+
[Model Context Protocol (MCP)](https://modelcontextprotocol.io/) tools can be used with Claude Code hooks. When MCP servers are configured, their tools become available and can be intercepted by hooks just like built-in tools.
|
|
752
|
+
|
|
753
|
+
### MCP Tool Naming Convention
|
|
754
|
+
|
|
755
|
+
MCP tools follow a specific naming pattern: `mcp__<server-name>__<tool-name>`
|
|
756
|
+
|
|
757
|
+
**Example MCP tool names:**
|
|
758
|
+
- `mcp__filesystem__read_file`
|
|
759
|
+
- `mcp__github__create_issue`
|
|
760
|
+
- `mcp__database__query`
|
|
761
|
+
|
|
762
|
+
### Configuring Hooks for MCP Tools
|
|
763
|
+
|
|
764
|
+
You can use matchers to target specific MCP tools or all tools from a server:
|
|
765
|
+
|
|
766
|
+
```json
|
|
767
|
+
{
|
|
768
|
+
"hooks": {
|
|
769
|
+
"PreToolUse": [
|
|
770
|
+
{
|
|
771
|
+
"matcher": "mcp__github__.*",
|
|
772
|
+
"hooks": [
|
|
773
|
+
{
|
|
774
|
+
"type": "command",
|
|
775
|
+
"command": "~/.claude/hooks/github_guard.rb"
|
|
776
|
+
}
|
|
777
|
+
]
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
"matcher": "mcp__.*__create.*|mcp__.*__delete.*",
|
|
781
|
+
"hooks": [
|
|
782
|
+
{
|
|
783
|
+
"type": "command",
|
|
784
|
+
"command": "~/.claude/hooks/destructive_operation_guard.rb"
|
|
785
|
+
}
|
|
786
|
+
]
|
|
787
|
+
}
|
|
788
|
+
]
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
### Common MCP Tool Patterns
|
|
794
|
+
|
|
795
|
+
| Pattern | Matches |
|
|
796
|
+
|---------|---------|
|
|
797
|
+
| `mcp__*__*` | All MCP tools from all servers |
|
|
798
|
+
| `mcp__github__*` | All tools from the github server |
|
|
799
|
+
| `mcp__*__read*` | All read operations from any server |
|
|
800
|
+
| `mcp__.*__create.*\|mcp__.*__delete.*` | All create/delete operations |
|
|
801
|
+
|
|
802
|
+
### Best Practices for MCP Tools
|
|
803
|
+
|
|
804
|
+
1. **Use regex matchers** - MCP tool names are predictable, making regex patterns very effective
|
|
805
|
+
2. **Guard destructive operations** - Always review create/delete/update operations
|
|
806
|
+
3. **Server-specific rules** - Different MCP servers may need different security policies
|
|
807
|
+
4. **Log MCP tool usage** - Track which external tools are being used
|
|
808
|
+
5. **Test with your MCP servers** - Tool names vary by server implementation
|
|
809
|
+
|
|
810
|
+
See the [official MCP documentation](https://modelcontextprotocol.io/) for more information about MCP servers and tools.
|
|
726
811
|
|
|
727
812
|
## 🚨 Advices
|
|
728
813
|
|
|
@@ -732,124 +817,103 @@ See the [plugin components reference](https://docs.claude.com/en/docs/claude-cod
|
|
|
732
817
|
|
|
733
818
|
## ⚠️ Troubleshooting
|
|
734
819
|
|
|
735
|
-
### Make your
|
|
820
|
+
### Make your hook scripts executable
|
|
736
821
|
|
|
737
822
|
Don't forget to make the scripts called from `settings.json` executable:
|
|
738
823
|
|
|
739
824
|
```bash
|
|
740
|
-
chmod +x ~/.claude/hooks/
|
|
825
|
+
chmod +x ~/.claude/hooks/my_hook.rb
|
|
741
826
|
```
|
|
742
827
|
|
|
743
828
|
|
|
744
829
|
## 🧪 CLI Debugging
|
|
745
830
|
|
|
746
|
-
|
|
831
|
+
`ClaudeHooks::CLI` provides two helpers: `run_hook` (for production use) and `test_runner`/`run_with_sample_data` (for local testing with custom input).
|
|
747
832
|
|
|
748
|
-
###
|
|
833
|
+
### CLI.run_hook
|
|
749
834
|
|
|
750
|
-
|
|
835
|
+
`CLI.run_hook` is what you put at the bottom of every simple hook script. It reads JSON from STDIN, runs your hook, handles errors, and calls `output_and_exit` with the right exit code.
|
|
751
836
|
|
|
752
837
|
```ruby
|
|
753
|
-
#
|
|
754
|
-
|
|
755
|
-
begin
|
|
756
|
-
require 'json'
|
|
757
|
-
input_data = JSON.parse(STDIN.read)
|
|
758
|
-
hook = MyHook.new(input_data)
|
|
759
|
-
result = hook.call
|
|
760
|
-
puts JSON.generate(result)
|
|
761
|
-
rescue StandardError => e
|
|
762
|
-
STDERR.puts "Error: #{e.message}"
|
|
763
|
-
puts JSON.generate({
|
|
764
|
-
continue: false,
|
|
765
|
-
stopReason: "Error: #{e.message}",
|
|
766
|
-
suppressOutput: false
|
|
767
|
-
})
|
|
768
|
-
exit 1
|
|
769
|
-
end
|
|
770
|
-
end
|
|
838
|
+
# Single hook (most common)
|
|
839
|
+
ClaudeHooks::CLI.run_hook(MyHook)
|
|
771
840
|
```
|
|
772
841
|
|
|
773
|
-
|
|
842
|
+
It replaces the more verbose
|
|
774
843
|
|
|
775
844
|
```ruby
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
845
|
+
begin
|
|
846
|
+
# Read input from stdin
|
|
847
|
+
input_data = JSON.parse(STDIN.read)
|
|
848
|
+
|
|
849
|
+
hook = MyHook.new(input_data)
|
|
850
|
+
hook.call
|
|
851
|
+
hook.output_and_exit
|
|
852
|
+
rescue StandardError => e
|
|
853
|
+
# Non-blocking by default (fail-open): Claude continues as if the hook didn't run.
|
|
854
|
+
# Pass `on_error: :block` to CLI.run_hook to exit 2 (fail-closed) instead.
|
|
855
|
+
STDERR.puts JSON.generate({
|
|
856
|
+
continue: false,
|
|
857
|
+
stopReason: "Hook execution error: #{e.message}",
|
|
858
|
+
suppressOutput: false
|
|
859
|
+
})
|
|
860
|
+
exit 1
|
|
779
861
|
end
|
|
780
862
|
```
|
|
781
863
|
|
|
782
|
-
|
|
864
|
+
#### on_error: fail-open vs fail-closed
|
|
783
865
|
|
|
784
|
-
|
|
866
|
+
By default, if your hook raises an unexpected exception, `CLI.run_hook` exits 1 (non-blocking) — Claude continues as if the hook didn't run. This is **fail-open**.
|
|
785
867
|
|
|
786
|
-
|
|
787
|
-
if __FILE__ == $0
|
|
788
|
-
ClaudeHooks::CLI.test_runner(MyHook) do |input_data|
|
|
789
|
-
input_data['debug_mode'] = true
|
|
790
|
-
input_data['custom_field'] = 'test_value'
|
|
791
|
-
input_data['user_name'] = 'TestUser'
|
|
792
|
-
end
|
|
793
|
-
end
|
|
794
|
-
```
|
|
868
|
+
For security or policy hooks (`PreToolUse` guards, prompt filters, etc.) you almost certainly want **fail-closed** instead — a crash should block the action, not silently pass it through:
|
|
795
869
|
|
|
796
|
-
### Testing Methods
|
|
797
|
-
|
|
798
|
-
#### 1. Test with STDIN (default)
|
|
799
870
|
```ruby
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
```
|
|
871
|
+
# Default: hook crash is non-blocking — Claude continues anyway (exit 1)
|
|
872
|
+
ClaudeHooks::CLI.run_hook(MyHook)
|
|
803
873
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
ClaudeHooks::CLI.run_with_sample_data(MyHook, { 'prompt' => 'test prompt' })
|
|
807
|
-
# Provides default values, no STDIN needed
|
|
808
|
-
```
|
|
874
|
+
# Fail-closed: hook crash blocks the action (exit 2)
|
|
875
|
+
ClaudeHooks::CLI.run_hook(MyHook, on_error: :block)
|
|
809
876
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
input_data['prompt'] = 'Custom test prompt'
|
|
814
|
-
input_data['debug'] = true
|
|
877
|
+
# Also works with block form
|
|
878
|
+
ClaudeHooks::CLI.run_hook(on_error: :block) do |input_data|
|
|
879
|
+
# ...
|
|
815
880
|
end
|
|
816
881
|
```
|
|
817
882
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
```ruby
|
|
821
|
-
#!/usr/bin/env ruby
|
|
883
|
+
> [!WARNING]
|
|
884
|
+
> If you're writing a `PreToolUse` or `UserPromptSubmit` hook that enforces security policy, use `on_error: :block`. Without it, a Ruby exception (network timeout, nil reference, etc.) will silently allow the action through.
|
|
822
885
|
|
|
823
|
-
|
|
886
|
+
### CLI.test_runner — local testing
|
|
824
887
|
|
|
825
|
-
|
|
826
|
-
def call
|
|
827
|
-
log "Debug mode: #{input_data['debug_mode']}"
|
|
828
|
-
log "Processing: #{prompt}"
|
|
888
|
+
Use `test_runner` when running the script directly (outside of Claude Code) to inject custom input data:
|
|
829
889
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
890
|
+
```ruby
|
|
891
|
+
# At the bottom of your hook file, guarded so it only runs directly:
|
|
892
|
+
if __FILE__ == $0
|
|
893
|
+
ClaudeHooks::CLI.test_runner(MyHook) do |input_data|
|
|
894
|
+
input_data['debug_mode'] = true
|
|
895
|
+
input_data['prompt'] = 'Test prompt'
|
|
835
896
|
end
|
|
836
897
|
end
|
|
837
898
|
|
|
838
|
-
#
|
|
899
|
+
# Or test with synthetic data (no STDIN needed):
|
|
839
900
|
if __FILE__ == $0
|
|
840
|
-
ClaudeHooks::CLI.
|
|
841
|
-
input_data['debug_mode'] = true
|
|
842
|
-
end
|
|
901
|
+
ClaudeHooks::CLI.run_with_sample_data(MyHook, { 'prompt' => 'test prompt' })
|
|
843
902
|
end
|
|
844
903
|
```
|
|
845
904
|
|
|
905
|
+
Test with real STDIN:
|
|
906
|
+
```bash
|
|
907
|
+
echo '{"session_id":"test","prompt":"Hello"}' | ruby my_hook.rb
|
|
908
|
+
```
|
|
909
|
+
|
|
846
910
|
## 🐛 Debugging
|
|
847
911
|
|
|
848
|
-
### Test
|
|
912
|
+
### Test a hook script directly
|
|
849
913
|
|
|
850
914
|
```bash
|
|
851
915
|
# Test with sample data
|
|
852
|
-
echo '{"session_id": "test", "transcript_path": "/tmp/transcript", "cwd": "/tmp", "hook_event_name": "UserPromptSubmit", "user_prompt": "Hello Claude"}' | CLAUDE_PROJECT_DIR=$(pwd) ruby ~/.claude/hooks/
|
|
916
|
+
echo '{"session_id": "test", "transcript_path": "/tmp/transcript", "cwd": "/tmp", "hook_event_name": "UserPromptSubmit", "user_prompt": "Hello Claude"}' | CLAUDE_PROJECT_DIR=$(pwd) ruby ~/.claude/hooks/user_prompt_submit.rb
|
|
853
917
|
```
|
|
854
918
|
|
|
855
919
|
## 🧪 Development & Contributing
|