claude_hooks 0.1.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 +7 -0
- data/CHANGELOG.md +31 -0
- data/README.md +777 -0
- data/WHY.md +65 -0
- data/claude_hooks.gemspec +39 -0
- data/example_dotclaude/commands/.gitkeep +0 -0
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit/append_rules.rb +66 -0
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit/log_user_prompt.rb +50 -0
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit.rb +44 -0
- data/example_dotclaude/hooks/handlers/user_prompt_submit/append_rules.rb +66 -0
- data/example_dotclaude/hooks/handlers/user_prompt_submit/log_user_prompt.rb +50 -0
- data/example_dotclaude/settings.json +23 -0
- data/lib/claude_hooks/base.rb +157 -0
- data/lib/claude_hooks/configuration.rb +145 -0
- data/lib/claude_hooks/logger.rb +66 -0
- data/lib/claude_hooks/notification.rb +22 -0
- data/lib/claude_hooks/post_tool_use.rb +55 -0
- data/lib/claude_hooks/pre_compact.rb +42 -0
- data/lib/claude_hooks/pre_tool_use.rb +88 -0
- data/lib/claude_hooks/session_start.rb +58 -0
- data/lib/claude_hooks/stop.rb +61 -0
- data/lib/claude_hooks/subagent_stop.rb +11 -0
- data/lib/claude_hooks/user_prompt_submit.rb +73 -0
- data/lib/claude_hooks/version.rb +5 -0
- data/lib/claude_hooks.rb +18 -0
- metadata +115 -0
data/README.md
ADDED
@@ -0,0 +1,777 @@
|
|
1
|
+
# Ruby DSL for Claude Code hooks
|
2
|
+
|
3
|
+
A Ruby DSL (Domain Specific Language) for creating Claude Code hooks. This will hopefully make creating and configuring new hooks way easier.
|
4
|
+
|
5
|
+
[**Why use this instead of writing bash, or simple ruby scripts?**](WHY.md)
|
6
|
+
|
7
|
+
## 🚀 Quick Start
|
8
|
+
|
9
|
+
> [!TIP]
|
10
|
+
> An example is available in [`example_dotclaude/hooks/`](example_dotclaude/hooks/)
|
11
|
+
|
12
|
+
Here's how to create a simple hook:
|
13
|
+
|
14
|
+
1. **Install the gem:**
|
15
|
+
```bash
|
16
|
+
gem install claude_hooks
|
17
|
+
```
|
18
|
+
|
19
|
+
1. **Create a simple hook script**
|
20
|
+
```ruby
|
21
|
+
#!/usr/bin/env ruby
|
22
|
+
require 'claude_hooks'
|
23
|
+
|
24
|
+
# Inherit from the right hook type class to get access to helper methods
|
25
|
+
class AddContextAfterPrompt < ClaudeHooks::UserPromptSubmit
|
26
|
+
def call
|
27
|
+
log "User asked: #{prompt}"
|
28
|
+
add_context!("Remember to be extra helpful!")
|
29
|
+
output_data
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Run the hook
|
34
|
+
if __FILE__ == $0
|
35
|
+
# Read Claude Code's input data from STDIN
|
36
|
+
input_data = JSON.parse(STDIN.read)
|
37
|
+
|
38
|
+
hook = AddContextAfterPrompt.new(input_data)
|
39
|
+
output = hook.call
|
40
|
+
|
41
|
+
puts JSON.generate(output)
|
42
|
+
exit 0
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
3. ⚠️ **Make it executable (and test it)**
|
47
|
+
```bash
|
48
|
+
chmod +x add_context_after_prompt.rb
|
49
|
+
echo '{"session_id":"test","prompt":"Hello!"}' | ruby add_context_after_prompt.rb
|
50
|
+
```
|
51
|
+
|
52
|
+
4. **Register it in your `.claude/settings.json`**
|
53
|
+
```json
|
54
|
+
{
|
55
|
+
"hooks": {
|
56
|
+
"UserPromptSubmit": [{
|
57
|
+
"matcher": "",
|
58
|
+
"hooks": [
|
59
|
+
{
|
60
|
+
"type": "command",
|
61
|
+
"command": "path/to/your/hook.rb"
|
62
|
+
}
|
63
|
+
]
|
64
|
+
}]
|
65
|
+
}
|
66
|
+
}
|
67
|
+
```
|
68
|
+
That's it! Your hook will now add context to every user prompt. 🎉
|
69
|
+
|
70
|
+
> [!TIP]
|
71
|
+
> This was a very simple example but we recommend using the entrypoints/handlers architecture [described below](#recommended-structure-for-your-claudehooks-directory) to create more complex hook systems.
|
72
|
+
|
73
|
+
## 📦 Installation
|
74
|
+
|
75
|
+
Add to your Gemfile (you can add a Gemfile in your `.claude` directory if needed):
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
gem 'claude_hooks'
|
79
|
+
```
|
80
|
+
|
81
|
+
And then execute:
|
82
|
+
|
83
|
+
```bash
|
84
|
+
$ bundle install
|
85
|
+
```
|
86
|
+
|
87
|
+
Or install it globally:
|
88
|
+
|
89
|
+
```bash
|
90
|
+
$ gem install claude_hooks
|
91
|
+
```
|
92
|
+
|
93
|
+
### 🔧 Configuration
|
94
|
+
|
95
|
+
This gem uses either environment variables or a global configuration file.
|
96
|
+
|
97
|
+
|
98
|
+
#### Required Configuration Options
|
99
|
+
|
100
|
+
| Option | Description | Default |
|
101
|
+
|--------|-------------|---------|
|
102
|
+
| `baseDir` | Base directory for all Claude files | `~/.claude` |
|
103
|
+
| `logDirectory` | Directory for logs (relative to baseDir) | `logs` |
|
104
|
+
|
105
|
+
#### Environment Variables (Preferred)
|
106
|
+
|
107
|
+
The gem uses environment variables with the `RUBY_CLAUDE_HOOKS_` prefix for configuration:
|
108
|
+
|
109
|
+
```bash
|
110
|
+
export RUBY_CLAUDE_HOOKS_BASE_DIR="~/.claude" # Default: ~/.claude
|
111
|
+
export RUBY_CLAUDE_HOOKS_LOG_DIR="logs" # Default: logs (relative to base_dir)
|
112
|
+
|
113
|
+
# You can add any custom configuration
|
114
|
+
export RUBY_CLAUDE_HOOKS_API_KEY="your-api-key"
|
115
|
+
export RUBY_CLAUDE_HOOKS_DEBUG_MODE="true"
|
116
|
+
export RUBY_CLAUDE_HOOKS_USER_NAME="Gabriel"
|
117
|
+
```
|
118
|
+
|
119
|
+
#### Configuration File
|
120
|
+
|
121
|
+
You can choose to use a global configuration file by setting it up in `~/.claude/config/config.json`.
|
122
|
+
The gem will read from it as fallback for any missing environment variables.
|
123
|
+
|
124
|
+
```json
|
125
|
+
{
|
126
|
+
"baseDir": "~/.claude",
|
127
|
+
"logDirectory": "logs",
|
128
|
+
"apiKey": "your-api-key",
|
129
|
+
"debugMode": true,
|
130
|
+
"userName": "Gabriel"
|
131
|
+
}
|
132
|
+
```
|
133
|
+
|
134
|
+
#### Accessing Custom Configuration
|
135
|
+
|
136
|
+
You can access any configuration value in your handlers:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class MyHandler < ClaudeHooks::UserPromptSubmit
|
140
|
+
def call
|
141
|
+
# Access built-in config
|
142
|
+
log "Base dir: #{config.base_dir}"
|
143
|
+
log "Logs dir: #{config.logs_directory}"
|
144
|
+
|
145
|
+
# Access custom config via method calls
|
146
|
+
log "API Key: #{config.api_key}"
|
147
|
+
log "Debug mode: #{config.debug_mode}"
|
148
|
+
log "User: #{config.user_name}"
|
149
|
+
|
150
|
+
# Or use get_config_value for more control
|
151
|
+
user_name = config.get_config_value('USER_NAME', 'userName', )
|
152
|
+
log "Username: #{user_name}"
|
153
|
+
|
154
|
+
output_data
|
155
|
+
end
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
**Configuration Priority:** Environment variables always take precedence over config file values.
|
160
|
+
|
161
|
+
## 📖 Table of Contents
|
162
|
+
|
163
|
+
- [Ruby DSL for Claude Code hooks](#ruby-dsl-for-claude-code-hooks)
|
164
|
+
- [🚀 Quick Start](#-quick-start)
|
165
|
+
- [📦 Installation](#-installation)
|
166
|
+
- [🔧 Configuration](#-configuration)
|
167
|
+
- [Required Configuration Options](#required-configuration-options)
|
168
|
+
- [Environment Variables (Preferred)](#environment-variables-preferred)
|
169
|
+
- [Configuration File](#configuration-file)
|
170
|
+
- [Accessing Custom Configuration](#accessing-custom-configuration)
|
171
|
+
- [📖 Table of Contents](#-table-of-contents)
|
172
|
+
- [🏗️ Architecture](#️-architecture)
|
173
|
+
- [Core Components](#core-components)
|
174
|
+
- [Recommended structure for your .claude/hooks/ directory](#recommended-structure-for-your-claudehooks-directory)
|
175
|
+
- [🪝 Hook Types](#-hook-types)
|
176
|
+
- [🚀 Claude Hook Flow](#-claude-hook-flow)
|
177
|
+
- [A very simplified view of how a hook works in Claude Code](#a-very-simplified-view-of-how-a-hook-works-in-claude-code)
|
178
|
+
- [🔄 Claude Hook Execution Flow](#-claude-hook-execution-flow)
|
179
|
+
- [Basic Hook Handler Structure](#basic-hook-handler-structure)
|
180
|
+
- [Input Fields](#input-fields)
|
181
|
+
- [📚 API Reference](#-api-reference)
|
182
|
+
- [Common API Methods](#common-api-methods)
|
183
|
+
- [Input Methods](#input-methods)
|
184
|
+
- [Output Methods](#output-methods)
|
185
|
+
- [Class Output Methods](#class-output-methods)
|
186
|
+
- [Utility Methods](#utility-methods)
|
187
|
+
- [UserPromptSubmit API](#userpromptsubmit-api)
|
188
|
+
- [Input Methods](#input-methods-1)
|
189
|
+
- [Output Methods](#output-methods-1)
|
190
|
+
- [PreToolUse API](#pretooluse-api)
|
191
|
+
- [Input Methods](#input-methods-2)
|
192
|
+
- [Output Methods](#output-methods-2)
|
193
|
+
- [PostToolUse API](#posttooluse-api)
|
194
|
+
- [Input Methods](#input-methods-3)
|
195
|
+
- [Output Methods](#output-methods-3)
|
196
|
+
- [Notification API](#notification-api)
|
197
|
+
- [Input Methods](#input-methods-4)
|
198
|
+
- [Output Methods](#output-methods-4)
|
199
|
+
- [Stop API](#stop-api)
|
200
|
+
- [Input Methods](#input-methods-5)
|
201
|
+
- [Output Methods](#output-methods-5)
|
202
|
+
- [SubagentStop API](#subagentstop-api)
|
203
|
+
- [Input Methods](#input-methods-6)
|
204
|
+
- [Output Methods](#output-methods-6)
|
205
|
+
- [PreCompact API](#precompact-api)
|
206
|
+
- [Input Methods](#input-methods-7)
|
207
|
+
- [Output Methods](#output-methods-7)
|
208
|
+
- [Utility Methods](#utility-methods-1)
|
209
|
+
- [SessionStart API](#sessionstart-api)
|
210
|
+
- [Input Methods](#input-methods-8)
|
211
|
+
- [Output Methods](#output-methods-8)
|
212
|
+
- [Configuration and Utility Methods](#configuration-and-utility-methods)
|
213
|
+
- [Configuration Methods](#configuration-methods)
|
214
|
+
- [Utility Methods](#utility-methods-2)
|
215
|
+
- [📝 Logging](#-logging)
|
216
|
+
- [Log File Location](#log-file-location)
|
217
|
+
- [Log Output Format](#log-output-format)
|
218
|
+
- [📝 Example: Tool usage monitor](#-example-tool-usage-monitor)
|
219
|
+
- [🔄 Hook Output](#-hook-output)
|
220
|
+
- [🔄 Hook Output Merging](#-hook-output-merging)
|
221
|
+
- [🚪 Hook Exit Codes](#-hook-exit-codes)
|
222
|
+
- [Pattern 1: Simple Exit Codes](#pattern-1-simple-exit-codes)
|
223
|
+
- [Example: Success](#example-success)
|
224
|
+
- [Example: Error](#example-error)
|
225
|
+
- [🚨 Advices](#-advices)
|
226
|
+
- [⚠️ Troubleshooting](#️-troubleshooting)
|
227
|
+
- [Make your entrypoint scripts executable](#make-your-entrypoint-scripts-executable)
|
228
|
+
- [🐛 Debugging](#-debugging)
|
229
|
+
- [Test an individual entrypoint](#test-an-individual-entrypoint)
|
230
|
+
|
231
|
+
|
232
|
+
## 🏗️ Architecture
|
233
|
+
|
234
|
+
### Core Components
|
235
|
+
|
236
|
+
1. **`ClaudeHooks::Base`** - Base class with common functionality (logging, config, validation)
|
237
|
+
2. **Hook Handler Classes** - Self-contained classes (`ClaudeHooks::UserPromptSubmit`, `ClaudeHooks::PreToolUse`, `ClaudeHooks::PostToolUse`, etc.)
|
238
|
+
3. **Logger** - Dedicated logging class with multiline block support
|
239
|
+
4. **Configuration** - Shared configuration management via `ClaudeHooks::Configuration`
|
240
|
+
|
241
|
+
### Recommended structure for your .claude/hooks/ directory
|
242
|
+
|
243
|
+
```
|
244
|
+
.claude/hooks/
|
245
|
+
├── entrypoints/ # Main entry points
|
246
|
+
│ ├── notification.rb
|
247
|
+
│ ├── pre_tool_use.rb
|
248
|
+
│ ├── post_tool_use.rb
|
249
|
+
│ ├── pre_compact.rb
|
250
|
+
│ ├── session_start.rb
|
251
|
+
│ ├── stop.rb
|
252
|
+
│ └── subagent_stop.rb
|
253
|
+
|
|
254
|
+
└── handlers/ # Hook handlers for specific hook type
|
255
|
+
├── user_prompt_submit/
|
256
|
+
│ ├── append_rules.rb
|
257
|
+
│ └── log_user_prompt.rb
|
258
|
+
├── pre_tool_use/
|
259
|
+
│ └── tool_monitor.rb
|
260
|
+
└── ...
|
261
|
+
```
|
262
|
+
|
263
|
+
## 🪝 Hook Types
|
264
|
+
|
265
|
+
The framework supports the following hook types:
|
266
|
+
|
267
|
+
| Hook Type | Class | Description |
|
268
|
+
|-----------|-------|-------------|
|
269
|
+
| **SessionStart** | `ClaudeHooks::SessionStart` | Hooks that run when Claude Code starts a new session or resumes |
|
270
|
+
| **UserPromptSubmit** | `ClaudeHooks::UserPromptSubmit` | Hooks that run before the user's prompt is processed |
|
271
|
+
| **Notification** | `ClaudeHooks::Notification` | Hooks that run when Claude Code sends notifications |
|
272
|
+
| **PreToolUse** | `ClaudeHooks::PreToolUse` | Hooks that run before a tool is used |
|
273
|
+
| **PostToolUse** | `ClaudeHooks::PostToolUse` | Hooks that run after a tool is used |
|
274
|
+
| **Stop** | `ClaudeHooks::Stop` | Hooks that run when Claude Code finishes responding |
|
275
|
+
| **SubagentStop** | `ClaudeHooks::SubagentStop` | Hooks that run when subagent tasks complete |
|
276
|
+
| **PreCompact** | `ClaudeHooks::PreCompact` | Hooks that run before transcript compaction |
|
277
|
+
|
278
|
+
## 🚀 Claude Hook Flow
|
279
|
+
|
280
|
+
### A very simplified view of how a hook works in Claude Code
|
281
|
+
|
282
|
+
```mermaid
|
283
|
+
graph LR
|
284
|
+
A[Hook triggers] --> B[JSON from STDIN] --> C[Hook does its thing] --> D[JSON to STDOUT or STDERR]
|
285
|
+
```
|
286
|
+
|
287
|
+
### 🔄 Claude Hook Execution Flow
|
288
|
+
|
289
|
+
1. An entrypoint for a hook is set in `~/.claude/settings.json`
|
290
|
+
2. Claude Code calls the entrypoint script (e.g., `hooks/entrypoints/pre_tool_use.rb`)
|
291
|
+
3. The entrypoint script reads STDIN and coordinates multiple **hook handlers**
|
292
|
+
4. Each **hook handler** executes and returns its output data
|
293
|
+
5. The entrypoint script combines/processes outputs from multiple **hook handlers**
|
294
|
+
6. And then returns final JSON response to Claude Code
|
295
|
+
|
296
|
+
```mermaid
|
297
|
+
graph TD
|
298
|
+
A[🔧 Hook Configuration<br/>settings.json] --> B
|
299
|
+
B[🤖 Claude Code<br/><em>User submits prompt</em>] --> C[📋 Entrypoint<br />entrypoints/user_prompt_submit.rb]
|
300
|
+
|
301
|
+
C --> D[📋 Entrypoint<br />Parses JSON from STDIN]
|
302
|
+
D --> E[📋 Entrypoint<br />Calls hook handlers]
|
303
|
+
|
304
|
+
E --> F[📝 AppendContextRules.call<br/><em>Returns output_data</em>]
|
305
|
+
E --> G[📝 PromptGuard.call<br/><em>Returns output_data</em>]
|
306
|
+
|
307
|
+
F --> J[📋 Entrypoint<br />Calls _ClaudeHooks::UserPromptSubmit.merge_outputs_ to 🔀 merge outputs]
|
308
|
+
G --> J
|
309
|
+
|
310
|
+
J --> K[📋 Entrypoint<br />Outputs JSON to STDOUT or STDERR]
|
311
|
+
K --> L[🤖 Yields back to Claude Code]
|
312
|
+
L --> B
|
313
|
+
```
|
314
|
+
|
315
|
+
### Basic Hook Handler Structure
|
316
|
+
|
317
|
+
```ruby
|
318
|
+
#!/usr/bin/env ruby
|
319
|
+
|
320
|
+
require 'claude_hooks'
|
321
|
+
|
322
|
+
class AddContextAfterPrompt < ClaudeHooks::UserPromptSubmit
|
323
|
+
def call
|
324
|
+
# Access input data
|
325
|
+
log do
|
326
|
+
"--- INPUT DATA ---"
|
327
|
+
"session_id: #{session_id}"
|
328
|
+
"cwd: #{cwd}"
|
329
|
+
"hook_event_name: #{hook_event_name}"
|
330
|
+
"prompt: #{current_prompt}"
|
331
|
+
"---"
|
332
|
+
end
|
333
|
+
|
334
|
+
log "Full conversation transcript: #{read_transcript}"
|
335
|
+
|
336
|
+
add_additional_context!("Some custom context")
|
337
|
+
|
338
|
+
# Block the prompt
|
339
|
+
if current_prompt.include?("bad word")
|
340
|
+
block_prompt!("Hmm no no no!")
|
341
|
+
log "Prompt blocked: #{current_prompt} because of bad word"
|
342
|
+
end
|
343
|
+
|
344
|
+
# Return output data
|
345
|
+
output_data
|
346
|
+
end
|
347
|
+
end
|
348
|
+
```
|
349
|
+
|
350
|
+
### Input Fields
|
351
|
+
|
352
|
+
The framework supports all existing hook types with their respective input fields:
|
353
|
+
|
354
|
+
| Hook Type | Input Fields |
|
355
|
+
|-----------|--------------|
|
356
|
+
| **Common** | `session_id`, `transcript_path`, `cwd`, `hook_event_name` |
|
357
|
+
| **UserPromptSubmit** | `prompt` |
|
358
|
+
| **PreToolUse** | `tool_name`, `tool_input` |
|
359
|
+
| **PostToolUse** | `tool_name`, `tool_input`, `tool_response` |
|
360
|
+
| **Notification** | `message` |
|
361
|
+
| **Stop** | `stop_hook_active` |
|
362
|
+
| **SubagentStop** | `stop_hook_active` |
|
363
|
+
| **PreCompact** | `trigger`, `custom_instructions` |
|
364
|
+
| **SessionStart** | `source` |
|
365
|
+
|
366
|
+
## 📚 API Reference
|
367
|
+
|
368
|
+
The whole purpose of those APIs is to simplify reading from STDIN and writing to STDOUT the way Claude Code expects you to.
|
369
|
+
|
370
|
+
### Common API Methods
|
371
|
+
|
372
|
+
Those methods are available in **all hook types** and are inherited from `ClaudeHooks::Base`:
|
373
|
+
|
374
|
+
#### Input Methods
|
375
|
+
Input methods are helpers to access data parsed from STDIN.
|
376
|
+
|
377
|
+
| Method | Description |
|
378
|
+
|--------|-------------|
|
379
|
+
| `input_data` | Input data reader |
|
380
|
+
| `session_id` | Get the current session ID |
|
381
|
+
| `transcript_path` | Get path to the transcript file |
|
382
|
+
| `cwd` | Get current working directory |
|
383
|
+
| `hook_event_name` | Get the hook event name |
|
384
|
+
| `read_transcript` | Read the transcript file |
|
385
|
+
| `transcript` | Alias for `read_transcript` |
|
386
|
+
|
387
|
+
#### Output Methods
|
388
|
+
Output methods are helpers to modify `output_data`.
|
389
|
+
|
390
|
+
| Method | Description |
|
391
|
+
|--------|-------------|
|
392
|
+
| `output_data` | Output data accessor |
|
393
|
+
| `stringify_output` | Generates a JSON string from `output_data` |
|
394
|
+
| `allow_continue!` | Allow Claude to continue (default) |
|
395
|
+
| `prevent_continue!(reason)` | Stop Claude with reason |
|
396
|
+
| `suppress_output!` | Hide stdout from transcript |
|
397
|
+
| `show_output!` | Show stdout in transcript (default) |
|
398
|
+
| `clear_specifics!` | Clear hook-specific output |
|
399
|
+
|
400
|
+
#### Class Output Methods
|
401
|
+
|
402
|
+
Each hook type provides a **class method** `merge_outputs` that will try to intelligently merge multiple hook results, e.g. `ClaudeHooks::UserPromptSubmit.merge_outputs(output1, output2, output3)`.
|
403
|
+
|
404
|
+
| Method | Description |
|
405
|
+
|--------|-------------|
|
406
|
+
| `merge_outputs(*outputs_data)` | Intelligently merge multiple outputs into a single output |
|
407
|
+
|
408
|
+
#### Utility Methods
|
409
|
+
| Method | Description |
|
410
|
+
|--------|-------------|
|
411
|
+
| `log(message, level: :info)` | Log to session-specific file (levels: :info, :warn, :error) |
|
412
|
+
|
413
|
+
### UserPromptSubmit API
|
414
|
+
|
415
|
+
Available when inheriting from `ClaudeHooks::UserPromptSubmit`:
|
416
|
+
|
417
|
+
#### Input Methods
|
418
|
+
| Method | Description |
|
419
|
+
|--------|-------------|
|
420
|
+
| `prompt` | Get the user's prompt text |
|
421
|
+
| `user_prompt` | Alias for `prompt` |
|
422
|
+
| `current_prompt` | Alias for `prompt` |
|
423
|
+
|
424
|
+
#### Output Methods
|
425
|
+
| Method | Description |
|
426
|
+
|--------|-------------|
|
427
|
+
| `add_additional_context!(context)` | Add context to the prompt |
|
428
|
+
| `add_context!(context)` | Alias for `add_additional_context!` |
|
429
|
+
| `empty_additional_context!` | Remove additional context |
|
430
|
+
| `block_prompt!(reason)` | Block the prompt from processing |
|
431
|
+
| `unblock_prompt!` | Unblock a previously blocked prompt |
|
432
|
+
|
433
|
+
### PreToolUse API
|
434
|
+
|
435
|
+
Available when inheriting from `ClaudeHooks::PreToolUse`:
|
436
|
+
|
437
|
+
#### Input Methods
|
438
|
+
| Method | Description |
|
439
|
+
|--------|-------------|
|
440
|
+
| `tool_name` | Get the name of the tool being used |
|
441
|
+
| `tool_input` | Get the input data for the tool |
|
442
|
+
|
443
|
+
#### Output Methods
|
444
|
+
| Method | Description |
|
445
|
+
|--------|-------------|
|
446
|
+
| `approve_tool!(reason)` | Explicitly approve tool usage |
|
447
|
+
| `block_tool!(reason)` | Block tool usage with feedback |
|
448
|
+
| `ask_for_permission!(reason)` | Request user permission |
|
449
|
+
|
450
|
+
### PostToolUse API
|
451
|
+
|
452
|
+
Available when inheriting from `ClaudeHooks::PostToolUse`:
|
453
|
+
|
454
|
+
#### Input Methods
|
455
|
+
| Method | Description |
|
456
|
+
|--------|-------------|
|
457
|
+
| `tool_name` | Get the name of the tool that was used |
|
458
|
+
| `tool_input` | Get the input that was passed to the tool |
|
459
|
+
| `tool_response` | Get the tool's response/output |
|
460
|
+
|
461
|
+
#### Output Methods
|
462
|
+
| Method | Description |
|
463
|
+
|--------|-------------|
|
464
|
+
| `block_tool!(reason)` | Block the tool result from being used |
|
465
|
+
| `approve_tool!(reason)` | Clear any previous block decision (allows tool result) |
|
466
|
+
|
467
|
+
### Notification API
|
468
|
+
|
469
|
+
Available when inheriting from `ClaudeHooks::Notification`:
|
470
|
+
|
471
|
+
#### Input Methods
|
472
|
+
| Method | Description |
|
473
|
+
|--------|-------------|
|
474
|
+
| `message` | Get the notification message content |
|
475
|
+
| `notification_message` | Alias for `message` |
|
476
|
+
|
477
|
+
#### Output Methods
|
478
|
+
Notifications are outside facing and do not have any specific output methods.
|
479
|
+
|
480
|
+
### Stop API
|
481
|
+
|
482
|
+
Available when inheriting from `ClaudeHooks::Stop`:
|
483
|
+
|
484
|
+
#### Input Methods
|
485
|
+
| Method | Description |
|
486
|
+
|--------|-------------|
|
487
|
+
| `stop_hook_active` | Check if Claude Code is already continuing as a result of a stop hook |
|
488
|
+
|
489
|
+
#### Output Methods
|
490
|
+
| Method | Description |
|
491
|
+
|--------|-------------|
|
492
|
+
| `continue_with_instructions!(instructions)` | Block Claude from stopping and provide instructions to continue |
|
493
|
+
| `block!(instructions)` | Alias for `continue_with_instructions!` |
|
494
|
+
| `ensure_stopping!` | Allow Claude to stop normally (default behavior) |
|
495
|
+
|
496
|
+
### SubagentStop API
|
497
|
+
|
498
|
+
Available when inheriting from `ClaudeHooks::SubagentStop` (inherits from `ClaudeHooks::Stop`):
|
499
|
+
|
500
|
+
#### Input Methods
|
501
|
+
| Method | Description |
|
502
|
+
|--------|-------------|
|
503
|
+
| `stop_hook_active` | Check if Claude Code is already continuing as a result of a stop hook |
|
504
|
+
|
505
|
+
#### Output Methods
|
506
|
+
| Method | Description |
|
507
|
+
|--------|-------------|
|
508
|
+
| `continue_with_instructions!(instructions)` | Block Claude from stopping and provide instructions to continue |
|
509
|
+
| `block!(instructions)` | Alias for `continue_with_instructions!` |
|
510
|
+
| `ensure_stopping!` | Allow Claude to stop normally (default behavior) |
|
511
|
+
|
512
|
+
### PreCompact API
|
513
|
+
|
514
|
+
Available when inheriting from `ClaudeHooks::PreCompact`:
|
515
|
+
|
516
|
+
#### Input Methods
|
517
|
+
| Method | Description |
|
518
|
+
|--------|-------------|
|
519
|
+
| `trigger` | Get the compaction trigger: `'manual'` or `'auto'` |
|
520
|
+
| `custom_instructions` | Get custom instructions (only available for manual trigger) |
|
521
|
+
|
522
|
+
#### Output Methods
|
523
|
+
No specific output methods are available to alter compaction behavior.
|
524
|
+
|
525
|
+
#### Utility Methods
|
526
|
+
| Method | Description |
|
527
|
+
|--------|-------------|
|
528
|
+
| `backup_transcript!(backup_file_path)` | Create a backup of the transcript at the specified path |
|
529
|
+
|
530
|
+
### SessionStart API
|
531
|
+
|
532
|
+
Available when inheriting from `ClaudeHooks::SessionStart`:
|
533
|
+
|
534
|
+
#### Input Methods
|
535
|
+
| Method | Description |
|
536
|
+
|--------|-------------|
|
537
|
+
| `source` | Get the session start source: `'startup'`, `'resume'`, or `'clear'` |
|
538
|
+
|
539
|
+
#### Output Methods
|
540
|
+
| Method | Description |
|
541
|
+
|--------|-------------|
|
542
|
+
| `add_additional_context!(context)` | Add contextual information for Claude's session |
|
543
|
+
| `add_context!(context)` | Alias for `add_additional_context!` |
|
544
|
+
| `empty_additional_context!` | Clear additional context |
|
545
|
+
|
546
|
+
### Configuration and Utility Methods
|
547
|
+
|
548
|
+
Available in all hooks via the base `ClaudeHooks::Base` class:
|
549
|
+
|
550
|
+
#### Configuration Methods
|
551
|
+
| Method | Description |
|
552
|
+
|--------|-------------|
|
553
|
+
| `base_dir` | Get the base Claude directory |
|
554
|
+
| `path_for(relative_path)` | Get absolute path relative to base dir |
|
555
|
+
| `config` | Access the full configuration object |
|
556
|
+
| `config.get_config_value(env_key, config_key, default)` | Get any config value with fallback |
|
557
|
+
| `config.logs_directory` | Get logs directory path |
|
558
|
+
| `config.your_custom_key` | Access any custom config via method_missing |
|
559
|
+
|
560
|
+
#### Utility Methods
|
561
|
+
| Method | Description |
|
562
|
+
|--------|-------------|
|
563
|
+
| `log(message, level: :info)` | Log to session-specific file (levels: :info, :warn, :error) |
|
564
|
+
| `log(level: :info) { block }` | Multiline logging with block support |
|
565
|
+
|
566
|
+
### 📝 Logging
|
567
|
+
|
568
|
+
`ClaudeHooks::Base` provides a **session logger** that will write logs to session-specific files.
|
569
|
+
|
570
|
+
```ruby
|
571
|
+
log "Simple message"
|
572
|
+
log "Error occurred", level: :error
|
573
|
+
log "Warning about something", level: :warn
|
574
|
+
|
575
|
+
log <<~TEXT
|
576
|
+
Configuration loaded successfully
|
577
|
+
Database connection established
|
578
|
+
System ready
|
579
|
+
TEXT
|
580
|
+
```
|
581
|
+
|
582
|
+
#### Log File Location
|
583
|
+
Logs are written to session-specific files in the configured log directory:
|
584
|
+
- **Defaults to**: `~/.claude/logs/hooks/session-{session_id}.log`
|
585
|
+
- **Configurable path**: Set via `config.json` → `logDirectory` or via `RUBY_CLAUDE_HOOKS_LOG_DIR` environment variable
|
586
|
+
|
587
|
+
#### Log Output Format
|
588
|
+
```
|
589
|
+
[2025-08-16 03:45:28] [INFO] [MyHookHandler] Starting execution
|
590
|
+
[2025-08-16 03:45:28] [ERROR] [MyHookHandler] Connection timeout
|
591
|
+
```
|
592
|
+
|
593
|
+
## 📝 Example: Tool usage monitor
|
594
|
+
|
595
|
+
Let's create a hook that will monitor tool usage and ask for permission before using dangerous tools.
|
596
|
+
|
597
|
+
First, register an entrypoint in `~/.claude/settings.json`:
|
598
|
+
|
599
|
+
```json
|
600
|
+
"hooks": {
|
601
|
+
"PreToolUse": [
|
602
|
+
{
|
603
|
+
"matcher": "",
|
604
|
+
"hooks": [
|
605
|
+
{
|
606
|
+
"type": "command",
|
607
|
+
"command": "~/.claude/hooks/entrypoints/pre_tool_use.rb"
|
608
|
+
}
|
609
|
+
]
|
610
|
+
}
|
611
|
+
],
|
612
|
+
}
|
613
|
+
```
|
614
|
+
|
615
|
+
Then, create your main entrypoint script and don't forget to make it executable:
|
616
|
+
```bash
|
617
|
+
touch ~/.claude/hooks/entrypoints/pre_tool_use.rb
|
618
|
+
chmod +x ~/.claude/hooks/entrypoints/pre_tool_use.rb
|
619
|
+
```
|
620
|
+
|
621
|
+
```ruby
|
622
|
+
#!/usr/bin/env ruby
|
623
|
+
|
624
|
+
require 'json'
|
625
|
+
require_relative '../handlers/pre_tool_use/tool_monitor'
|
626
|
+
|
627
|
+
begin
|
628
|
+
# Read input from stdin
|
629
|
+
input_data = JSON.parse(STDIN.read)
|
630
|
+
|
631
|
+
tool_monitor = ToolMonitor.new(input_data)
|
632
|
+
output = tool_monitor.call
|
633
|
+
|
634
|
+
# Any other hook scripts can be chained here
|
635
|
+
|
636
|
+
puts JSON.generate(output)
|
637
|
+
|
638
|
+
rescue JSON::ParserError => e
|
639
|
+
log "Error parsing JSON: #{e.message}", level: :error
|
640
|
+
puts JSON.generate({
|
641
|
+
continue: false,
|
642
|
+
stopReason: "JSON parsing error: #{e.message}",
|
643
|
+
suppressOutput: false
|
644
|
+
})
|
645
|
+
exit 0
|
646
|
+
rescue StandardError => e
|
647
|
+
log "Error in ToolMonitor hook: #{e.message}", level: :error
|
648
|
+
puts JSON.generate({
|
649
|
+
continue: false,
|
650
|
+
stopReason: "Hook execution error: #{e.message}",
|
651
|
+
suppressOutput: false
|
652
|
+
})
|
653
|
+
exit 0
|
654
|
+
end
|
655
|
+
```
|
656
|
+
|
657
|
+
Finally, create the handler that will be used to monitor tool usage.
|
658
|
+
|
659
|
+
```bash
|
660
|
+
touch ~/.claude/hooks/handlers/pre_tool_use/tool_monitor.rb
|
661
|
+
```
|
662
|
+
|
663
|
+
```ruby
|
664
|
+
#!/usr/bin/env ruby
|
665
|
+
|
666
|
+
require 'claude_hooks'
|
667
|
+
|
668
|
+
class ToolMonitor < ClaudeHooks::PreToolUse
|
669
|
+
DANGEROUS_TOOLS = %w[curl wget rm].freeze
|
670
|
+
|
671
|
+
def call
|
672
|
+
log "Monitoring tool usage: #{tool_name}"
|
673
|
+
|
674
|
+
if DANGEROUS_TOOLS.include?(tool_name)
|
675
|
+
log "Dangerous tool detected: #{tool_name}", level: :warn
|
676
|
+
ask_for_permission!("The tool '#{tool_name}' can impact your system. Allow?")
|
677
|
+
else
|
678
|
+
approve_tool!("Safe tool usage")
|
679
|
+
end
|
680
|
+
|
681
|
+
output_data
|
682
|
+
end
|
683
|
+
end
|
684
|
+
```
|
685
|
+
|
686
|
+
## 🔄 Hook Output
|
687
|
+
|
688
|
+
### 🔄 Hook Output Merging
|
689
|
+
|
690
|
+
Each hook script type provides a merging method `merge_outputs` that will try to intelligently merge multiple hook results:
|
691
|
+
|
692
|
+
```ruby
|
693
|
+
# Merge results from multiple UserPromptSubmit hooks
|
694
|
+
merged_result = ClaudeHooks::UserPromptSubmit.merge_outputs(output1, output2, output3)
|
695
|
+
|
696
|
+
# ClaudeHooks::UserPromptSubmit.merge_outputs follows the following merge logic:
|
697
|
+
# - continue: false wins (any hook script can stop execution)
|
698
|
+
# - suppressOutput: true wins (any hook script can suppress output)
|
699
|
+
# - decision: "block" wins (any hook script can block)
|
700
|
+
# - stopReason/reason: concatenated
|
701
|
+
# - additionalContext: joined
|
702
|
+
```
|
703
|
+
|
704
|
+
### 🚪 Hook Exit Codes
|
705
|
+
|
706
|
+
Claude Code hooks support multiple exit codes:
|
707
|
+
|
708
|
+
### Pattern 1: Simple Exit Codes
|
709
|
+
- **`exit 0`**: Success, allow the operation to continue
|
710
|
+
- **`exit 1`**: Non-blocking error, `STDERR` will be fed back to the user
|
711
|
+
- **`exit 2`**: Blocking error, `STDERR` will be fed back to Claude
|
712
|
+
|
713
|
+
**Hook-specific meanings:**
|
714
|
+
- **UserPromptSubmit**: `exit 1` blocks the prompt from being processed
|
715
|
+
- **PreToolUse**: `exit 1` blocks the tool, `exit 2` asks for permission
|
716
|
+
- **PostToolUse**: `exit 1` blocks the tool result from being used
|
717
|
+
|
718
|
+
|
719
|
+
### Example: Success
|
720
|
+
For the operation to continue for a UserPromptSubmit hook, you would return structured JSON data followed by `exit 0`:
|
721
|
+
|
722
|
+
```ruby
|
723
|
+
puts JSON.generate({
|
724
|
+
continue: true,
|
725
|
+
stopReason: "",
|
726
|
+
suppressOutput: false,
|
727
|
+
hookSpecificOutput: {
|
728
|
+
hookEventName: "UserPromptSubmit",
|
729
|
+
additionalContext: "context here"
|
730
|
+
}
|
731
|
+
})
|
732
|
+
exit 0
|
733
|
+
```
|
734
|
+
|
735
|
+
### Example: Error
|
736
|
+
|
737
|
+
For the operation to stop for a UserPromptSubmit hook, you would return structured JSON data followed by `exit 1`:
|
738
|
+
|
739
|
+
```ruby
|
740
|
+
$stderr.puts JSON.generate({
|
741
|
+
continue: false,
|
742
|
+
stopReason: "JSON parsing error: #{e.message}",
|
743
|
+
suppressOutput: false
|
744
|
+
})
|
745
|
+
exit 1
|
746
|
+
```
|
747
|
+
|
748
|
+
> [!WARNING]
|
749
|
+
> Don't forget to use `$stderr.puts` to output the JSON to STDERR.
|
750
|
+
|
751
|
+
|
752
|
+
## 🚨 Advices
|
753
|
+
|
754
|
+
1. **Logging**: Use `log()` method instead of `puts` to avoid interfering with JSON output
|
755
|
+
2. **Error Handling**: Hooks should handle their own errors and use the `log` method for debugging. For errors, don't forget to exit with the right exit code (1, 2) and output the JSON indicating the error to STDERR using `$stderr.puts`.
|
756
|
+
3. **Output Format**: Always return `output_data` or `nil` from your `call` method
|
757
|
+
4. **Path Management**: Use `path_for()` for all file operations relative to the Claude base directory
|
758
|
+
|
759
|
+
## ⚠️ Troubleshooting
|
760
|
+
|
761
|
+
### Make your entrypoint scripts executable
|
762
|
+
|
763
|
+
Don't forget to make the scripts called from `settings.json` executable:
|
764
|
+
|
765
|
+
```bash
|
766
|
+
chmod +x ~/.claude/hooks/entrypoints/user_prompt_submit.rb
|
767
|
+
```
|
768
|
+
|
769
|
+
|
770
|
+
## 🐛 Debugging
|
771
|
+
|
772
|
+
### Test an individual entrypoint
|
773
|
+
|
774
|
+
```bash
|
775
|
+
# Test with sample data
|
776
|
+
echo '{"session_id": "test", "transcript_path": "/tmp/transcript", "cwd": "/tmp", "hook_event_name": "UserPromptSubmit", "user_prompt": "Hello Claude"}' | ruby ~/.claude/hooks/entrypoints/user_prompt_submit.rb
|
777
|
+
```
|