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.
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
+ ```