claude_hooks 0.2.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +137 -0
  3. data/README.md +287 -356
  4. data/claude_hooks.gemspec +2 -2
  5. data/docs/1.0.0_MIGRATION_GUIDE.md +228 -0
  6. data/docs/API/COMMON.md +83 -0
  7. data/docs/API/NOTIFICATION.md +32 -0
  8. data/docs/API/POST_TOOL_USE.md +45 -0
  9. data/docs/API/PRE_COMPACT.md +39 -0
  10. data/docs/API/PRE_TOOL_USE.md +110 -0
  11. data/docs/API/SESSION_END.md +100 -0
  12. data/docs/API/SESSION_START.md +40 -0
  13. data/docs/API/STOP.md +47 -0
  14. data/docs/API/SUBAGENT_STOP.md +47 -0
  15. data/docs/API/USER_PROMPT_SUBMIT.md +47 -0
  16. data/{WHY.md → docs/WHY.md} +15 -8
  17. data/docs/external/claude-hooks-reference.md +34 -0
  18. data/example_dotclaude/hooks/entrypoints/pre_tool_use.rb +25 -0
  19. data/example_dotclaude/hooks/entrypoints/session_end.rb +35 -0
  20. data/example_dotclaude/hooks/entrypoints/user_prompt_submit.rb +17 -12
  21. data/example_dotclaude/hooks/handlers/pre_tool_use/github_guard.rb +253 -0
  22. data/example_dotclaude/hooks/handlers/session_end/cleanup_handler.rb +55 -0
  23. data/example_dotclaude/hooks/handlers/session_end/log_session_stats.rb +64 -0
  24. data/example_dotclaude/hooks/handlers/user_prompt_submit/append_rules.rb +3 -2
  25. data/example_dotclaude/hooks/handlers/user_prompt_submit/log_user_prompt.rb +2 -2
  26. data/example_dotclaude/plugins/README.md +175 -0
  27. data/example_dotclaude/settings.json +22 -0
  28. data/lib/claude_hooks/base.rb +16 -24
  29. data/lib/claude_hooks/cli.rb +75 -1
  30. data/lib/claude_hooks/logger.rb +0 -1
  31. data/lib/claude_hooks/output/base.rb +152 -0
  32. data/lib/claude_hooks/output/notification.rb +22 -0
  33. data/lib/claude_hooks/output/post_tool_use.rb +76 -0
  34. data/lib/claude_hooks/output/pre_compact.rb +20 -0
  35. data/lib/claude_hooks/output/pre_tool_use.rb +94 -0
  36. data/lib/claude_hooks/output/session_end.rb +24 -0
  37. data/lib/claude_hooks/output/session_start.rb +49 -0
  38. data/lib/claude_hooks/output/stop.rb +83 -0
  39. data/lib/claude_hooks/output/subagent_stop.rb +14 -0
  40. data/lib/claude_hooks/output/user_prompt_submit.rb +78 -0
  41. data/lib/claude_hooks/post_tool_use.rb +6 -12
  42. data/lib/claude_hooks/pre_tool_use.rb +0 -37
  43. data/lib/claude_hooks/session_end.rb +43 -0
  44. data/lib/claude_hooks/session_start.rb +0 -23
  45. data/lib/claude_hooks/stop.rb +8 -25
  46. data/lib/claude_hooks/user_prompt_submit.rb +0 -26
  47. data/lib/claude_hooks/version.rb +1 -1
  48. data/lib/claude_hooks.rb +15 -0
  49. metadata +37 -8
data/README.md CHANGED
@@ -1,15 +1,39 @@
1
1
  # Ruby DSL for Claude Code hooks
2
2
 
3
+ > [!IMPORTANT]
4
+ > v1.0.0 just released and is introducing breaking changes. Please read the [CHANGELOG](CHANGELOG.md) for more information.
5
+
6
+
3
7
  A Ruby DSL (Domain Specific Language) for creating Claude Code hooks. This will hopefully make creating and configuring new hooks way easier.
4
8
 
5
- [**Why use this instead of writing bash, or simple ruby scripts?**](WHY.md)
9
+ [**Why use this instead of writing bash, or simple ruby scripts?**](docs/WHY.md)
6
10
 
7
11
  > You might also be interested in my other project, a [Claude Code statusline](https://github.com/gabriel-dehan/claude_monitor_statusline) that shows your Claude usage in realtime, inside Claude Code ✨.
8
12
 
13
+
14
+ ## 📖 Table of Contents
15
+
16
+ - [Ruby DSL for Claude Code hooks](#ruby-dsl-for-claude-code-hooks)
17
+ - [📖 Table of Contents](#-table-of-contents)
18
+ - [🚀 Quick Start](#-quick-start)
19
+ - [📦 Installation](#-installation)
20
+ - [🏗️ Architecture](#️-architecture)
21
+ - [🪝 Hook Types](#-hook-types)
22
+ - [🚀 Claude Hook Flow](#-claude-hook-flow)
23
+ - [📚 API Reference](#-api-reference)
24
+ - [📝 Example: Tool usage monitor](#-example-tool-usage-monitor)
25
+ - [🔄 Hook Output](#-hook-output)
26
+ - [🔌 Plugin Hooks Support](#-plugin-hooks-support)
27
+ - [🚨 Advices](#-advices)
28
+ - [⚠️ Troubleshooting](#️-troubleshooting)
29
+ - [🧪 CLI Debugging](#-cli-debugging)
30
+ - [🐛 Debugging](#-debugging)
31
+ - [🧪 Development \& Contributing](#-development--contributing)
32
+
9
33
  ## 🚀 Quick Start
10
34
 
11
35
  > [!TIP]
12
- > An example is available in [`example_dotclaude/hooks/`](example_dotclaude/hooks/)
36
+ > 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)
13
37
 
14
38
  Here's how to create a simple hook:
15
39
 
@@ -29,7 +53,7 @@ class AddContextAfterPrompt < ClaudeHooks::UserPromptSubmit
29
53
  def call
30
54
  log "User asked: #{prompt}"
31
55
  add_context!("Remember to be extra helpful!")
32
- output_data
56
+ output
33
57
  end
34
58
  end
35
59
 
@@ -39,10 +63,11 @@ if __FILE__ == $0
39
63
  input_data = JSON.parse(STDIN.read)
40
64
 
41
65
  hook = AddContextAfterPrompt.new(input_data)
42
- output = hook.call
66
+ hook.call
43
67
 
44
- puts JSON.generate(output)
45
- exit 0
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
46
71
  end
47
72
  ```
48
73
 
@@ -76,16 +101,18 @@ That's it! Your hook will now add context to every user prompt. 🎉
76
101
 
77
102
  ## 📦 Installation
78
103
 
79
- Install it globally (simpler):
104
+ ### Install it globally (simpler):
80
105
 
81
106
  ```bash
82
107
  $ gem install claude_hooks
83
108
  ```
84
109
 
85
- **Note:** Claude Code itself will still use the system-installed gem, not the bundled version unless you use `bundle exec` to run it in your `.claude/settings.json`.
110
+ ### Using a Gemfile
86
111
 
87
- Or add it to your Gemfile (you can add a Gemfile in your `.claude` directory if needed):
112
+ > [!WARNING]
113
+ > Unless you use `bundle exec` in the command in your `.claude/settings.json`, Claude Code will use the system-installed gem, not the bundled version.
88
114
 
115
+ Add it to your Gemfile (you can add a Gemfile in your `.claude` directory if needed):
89
116
 
90
117
  ```ruby
91
118
  # .claude/Gemfile
@@ -100,9 +127,6 @@ And then run:
100
127
  $ bundle install
101
128
  ```
102
129
 
103
- > [!WARNING]
104
- > If you use a Gemfile, you need to use `bundle exec` to run your hooks in your `.claude/settings.json`.
105
-
106
130
  ### 🔧 Configuration
107
131
 
108
132
  Claude Hooks supports both home-level (`$HOME/.claude`) and project-level (`$CLAUDE_PROJECT_DIR/.claude`) directories. Claude Hooks specific config files (`config/config.json`) found in either directory will be merged together.
@@ -121,7 +145,7 @@ You can configure Claude Hooks through environment variables with the `RUBY_CLAU
121
145
 
122
146
  ```bash
123
147
  # Existing configuration options
124
- export RUBY_CLAUDE_HOOKS_LOG_DIR="logs" # Default: logs (relative to HOME/.claude)
148
+ export RUBY_CLAUDE_HOOKS_LOG_DIR="logs" # Default: logs (relative to $HOME/.claude)
125
149
  export RUBY_CLAUDE_HOOKS_CONFIG_MERGE_STRATEGY="project" # Config merge strategy: "project" or "home", default: "project"
126
150
  export RUBY_CLAUDE_HOOKS_BASE_DIR="~/.claude" # DEPRECATED: fallback base directory
127
151
 
@@ -193,97 +217,20 @@ class MyHandler < ClaudeHooks::UserPromptSubmit
193
217
  user_name = config.get_config_value('USER_NAME', 'userName')
194
218
  log "Username: #{user_name}"
195
219
 
196
- output_data
220
+ output
197
221
  end
198
222
  end
199
223
  ```
200
224
 
201
- ## 📖 Table of Contents
202
-
203
- - [Ruby DSL for Claude Code hooks](#ruby-dsl-for-claude-code-hooks)
204
- - [🚀 Quick Start](#-quick-start)
205
- - [📦 Installation](#-installation)
206
- - [🔧 Configuration](#-configuration)
207
- - [Environment Variables](#environment-variables)
208
- - [Configuration Files](#configuration-files)
209
- - [Configuration Merging](#configuration-merging)
210
- - [Accessing Configuration Variables](#accessing-configuration-variables)
211
- - [📖 Table of Contents](#-table-of-contents)
212
- - [🏗️ Architecture](#️-architecture)
213
- - [Core Components](#core-components)
214
- - [Recommended structure for your .claude/hooks/ directory](#recommended-structure-for-your-claudehooks-directory)
215
- - [🪝 Hook Types](#-hook-types)
216
- - [🚀 Claude Hook Flow](#-claude-hook-flow)
217
- - [A very simplified view of how a hook works in Claude Code](#a-very-simplified-view-of-how-a-hook-works-in-claude-code)
218
- - [🔄 Proposal: a more robust Claude Hook execution flow](#-proposal-a-more-robust-claude-hook-execution-flow)
219
- - [Basic Hook Handler Structure](#basic-hook-handler-structure)
220
- - [Input Fields](#input-fields)
221
- - [📚 API Reference](#-api-reference)
222
- - [Common API Methods](#common-api-methods)
223
- - [Input Methods](#input-methods)
224
- - [Output Methods](#output-methods)
225
- - [Class Output Methods](#class-output-methods)
226
- - [Configuration and Utility Methods](#configuration-and-utility-methods)
227
- - [Utility Methods](#utility-methods)
228
- - [Configuration Methods](#configuration-methods)
229
- - [UserPromptSubmit API](#userpromptsubmit-api)
230
- - [Input Methods](#input-methods-1)
231
- - [Output Methods](#output-methods-1)
232
- - [PreToolUse API](#pretooluse-api)
233
- - [Input Methods](#input-methods-2)
234
- - [Output Methods](#output-methods-2)
235
- - [PostToolUse API](#posttooluse-api)
236
- - [Input Methods](#input-methods-3)
237
- - [Output Methods](#output-methods-3)
238
- - [Notification API](#notification-api)
239
- - [Input Methods](#input-methods-4)
240
- - [Output Methods](#output-methods-4)
241
- - [Stop API](#stop-api)
242
- - [Input Methods](#input-methods-5)
243
- - [Output Methods](#output-methods-5)
244
- - [SubagentStop API](#subagentstop-api)
245
- - [Input Methods](#input-methods-6)
246
- - [Output Methods](#output-methods-6)
247
- - [PreCompact API](#precompact-api)
248
- - [Input Methods](#input-methods-7)
249
- - [Output Methods](#output-methods-7)
250
- - [Utility Methods](#utility-methods-1)
251
- - [SessionStart API](#sessionstart-api)
252
- - [Input Methods](#input-methods-8)
253
- - [Output Methods](#output-methods-8)
254
- - [📝 Logging](#-logging)
255
- - [Log File Location](#log-file-location)
256
- - [Log Output Format](#log-output-format)
257
- - [📝 Example: Tool usage monitor](#-example-tool-usage-monitor)
258
- - [🔄 Hook Output](#-hook-output)
259
- - [🔄 Hook Output Merging](#-hook-output-merging)
260
- - [🚪 Hook Exit Codes](#-hook-exit-codes)
261
- - [Pattern 1: Simple Exit Codes](#pattern-1-simple-exit-codes)
262
- - [Example: Success](#example-success)
263
- - [Example: Error](#example-error)
264
- - [🚨 Advices](#-advices)
265
- - [⚠️ Troubleshooting](#️-troubleshooting)
266
- - [Make your entrypoint scripts executable](#make-your-entrypoint-scripts-executable)
267
- - [🧪 CLI Debugging](#-cli-debugging)
268
- - [Basic Usage](#basic-usage)
269
- - [Customization with Blocks](#customization-with-blocks)
270
- - [Testing Methods](#testing-methods)
271
- - [1. Test with STDIN (default)](#1-test-with-stdin-default)
272
- - [2. Test with default sample data instead of STDIN](#2-test-with-default-sample-data-instead-of-stdin)
273
- - [3. Test with Sample Data + Customization](#3-test-with-sample-data--customization)
274
- - [Example Hook with CLI Testing](#example-hook-with-cli-testing)
275
- - [🐛 Debugging](#-debugging)
276
- - [Test an individual entrypoint](#test-an-individual-entrypoint)
277
-
278
-
279
225
  ## 🏗️ Architecture
280
226
 
281
227
  ### Core Components
282
228
 
283
229
  1. **`ClaudeHooks::Base`** - Base class with common functionality (logging, config, validation)
284
- 2. **Hook Handler Classes** - Self-contained classes (`ClaudeHooks::UserPromptSubmit`, `ClaudeHooks::PreToolUse`, `ClaudeHooks::PostToolUse`, etc.)
285
- 3. **Logger** - Dedicated logging class with multiline block support
230
+ 2. **Hook Handler Classes** - Self-contained classes (`ClaudeHooks::UserPromptSubmit`, `ClaudeHooks::PreToolUse`, etc.)
231
+ 3. **Output Classes** - `ClaudeHooks::Output::UserPromptSubmit`, etc... are output objects that handle intelligent merging of multiple outputs, as well as using the right exit codes and outputting to the proper stream (`STDIN` or `STDERR`) depending on the the hook state.
286
232
  4. **Configuration** - Shared configuration management via `ClaudeHooks::Configuration`
233
+ 5. **Logger** - Dedicated logging class with multiline block support
287
234
 
288
235
  ### Recommended structure for your .claude/hooks/ directory
289
236
 
@@ -303,6 +250,7 @@ end
303
250
  │ ├── append_rules.rb
304
251
  │ └── log_user_prompt.rb
305
252
  ├── pre_tool_use/
253
+ │ ├── github_guard.rb
306
254
  │ └── tool_monitor.rb
307
255
  └── ...
308
256
  ```
@@ -313,24 +261,35 @@ The framework supports the following hook types:
313
261
 
314
262
  | Hook Type | Class | Description |
315
263
  |-----------|-------|-------------|
316
- | **SessionStart** | `ClaudeHooks::SessionStart` | Hooks that run when Claude Code starts a new session or resumes |
317
- | **UserPromptSubmit** | `ClaudeHooks::UserPromptSubmit` | Hooks that run before the user's prompt is processed |
318
- | **Notification** | `ClaudeHooks::Notification` | Hooks that run when Claude Code sends notifications |
319
- | **PreToolUse** | `ClaudeHooks::PreToolUse` | Hooks that run before a tool is used |
320
- | **PostToolUse** | `ClaudeHooks::PostToolUse` | Hooks that run after a tool is used |
321
- | **Stop** | `ClaudeHooks::Stop` | Hooks that run when Claude Code finishes responding |
322
- | **SubagentStop** | `ClaudeHooks::SubagentStop` | Hooks that run when subagent tasks complete |
323
- | **PreCompact** | `ClaudeHooks::PreCompact` | Hooks that run before transcript compaction |
264
+ | **[SessionStart](docs/API/SESSION_START.md)** | `ClaudeHooks::SessionStart` | Hooks that run when Claude Code starts a new session, resumes, or compacts |
265
+ | **[UserPromptSubmit](docs/API/USER_PROMPT_SUBMIT.md)** | `ClaudeHooks::UserPromptSubmit` | Hooks that run before the user's prompt is processed |
266
+ | **[Notification](docs/API/NOTIFICATION.md)** | `ClaudeHooks::Notification` | Hooks that run when Claude Code sends notifications |
267
+ | **[PreToolUse](docs/API/PRE_TOOL_USE.md)** | `ClaudeHooks::PreToolUse` | Hooks that run before a tool is used |
268
+ | **[PostToolUse](docs/API/POST_TOOL_USE.md)** | `ClaudeHooks::PostToolUse` | Hooks that run after a tool is used |
269
+ | **[Stop](docs/API/STOP.md)** | `ClaudeHooks::Stop` | Hooks that run when Claude Code finishes responding |
270
+ | **[SubagentStop](docs/API/SUBAGENT_STOP.md)** | `ClaudeHooks::SubagentStop` | Hooks that run when subagent tasks complete |
271
+ | **[SessionEnd](docs/API/SESSION_END.md)** | `ClaudeHooks::SessionEnd` | Hooks that run when Claude Code sessions end |
272
+ | **[PreCompact](docs/API/PRE_COMPACT.md)** | `ClaudeHooks::PreCompact` | Hooks that run before transcript compaction |
324
273
 
325
274
  ## 🚀 Claude Hook Flow
326
275
 
327
276
  ### A very simplified view of how a hook works in Claude Code
328
277
 
278
+ Claude Code hooks in essence work in a very simple way:
279
+ - Claude Code passes data to the hook script through `STDIN`
280
+ - The hook uses the data to do its thing
281
+ - The hook outputs data to `STDOUT` or `STDERR` and then `exit`s with the proper code:
282
+ - `exit 0` for success
283
+ - `exit 1` for a non-blocking error
284
+ - `exit 2` for a blocking error (prevent Claude from continuing)
285
+
329
286
  ```mermaid
330
287
  graph LR
331
- A[Hook triggers] --> B[JSON from STDIN] --> C[Hook does its thing] --> D[JSON to STDOUT or STDERR] --> E[Yields back to Claude Code] --> A
288
+ 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
332
289
  ```
333
290
 
291
+ The main issue is that there are many different types of hooks and they each have different expectations regarding the data outputted to `STDIN` or `STDERR` and Claude Code will react differently for each specific exit code used depending on the hook type.
292
+
334
293
  ### 🔄 Proposal: a more robust Claude Hook execution flow
335
294
 
336
295
  1. An entrypoint for a hook is set in `~/.claude/settings.json`
@@ -338,7 +297,7 @@ graph LR
338
297
  3. The entrypoint script reads STDIN and coordinates multiple **hook handlers**
339
298
  4. Each **hook handler** executes and returns its output data
340
299
  5. The entrypoint script combines/processes outputs from multiple **hook handlers**
341
- 6. And then returns final JSON response to Claude Code
300
+ 6. And then returns final response to Claude Code with the correct exit code
342
301
 
343
302
  ```mermaid
344
303
  graph TD
@@ -348,13 +307,13 @@ graph TD
348
307
  C --> D[📋 Entrypoint<br />Parses JSON from STDIN]
349
308
  D --> E[📋 Entrypoint<br />Calls hook handlers]
350
309
 
351
- E --> F[📝 Handler<br />AppendContextRules.call<br/><em>Returns output_data</em>]
352
- E --> G[📝 Handler<br />PromptGuard.call<br/><em>Returns output_data</em>]
310
+ E --> F[📝 Handler<br />AppendContextRules.call<br/><em>Returns output</em>]
311
+ E --> G[📝 Handler<br />PromptGuard.call<br/><em>Returns output</em>]
353
312
 
354
- F --> J[📋 Entrypoint<br />Calls _ClaudeHooks::UserPromptSubmit.merge_outputs_ to 🔀 merge outputs]
313
+ F --> J[📋 Entrypoint<br />Calls _ClaudeHooks::Output::UserPromptSubmit.merge_ to 🔀 merge outputs]
355
314
  G --> J
356
315
 
357
- J --> K[📋 Entrypoint<br />Outputs JSON to STDOUT or STDERR]
316
+ J --> K[📋 Entrypoint<br />- Writes output to STDOUT or STDERR<br />- Uses correct exit code]
358
317
  K --> L[🤖 Yields back to Claude Code]
359
318
  L --> B
360
319
  ```
@@ -380,20 +339,48 @@ class AddContextAfterPrompt < ClaudeHooks::UserPromptSubmit
380
339
 
381
340
  log "Full conversation transcript: #{read_transcript}"
382
341
 
342
+ # Use a Hook state method to modify what's sent back to Claude Code
383
343
  add_additional_context!("Some custom context")
384
344
 
385
- # Block the prompt
345
+ # Control execution, for instance: block the prompt
386
346
  if current_prompt.include?("bad word")
387
347
  block_prompt!("Hmm no no no!")
388
348
  log "Prompt blocked: #{current_prompt} because of bad word"
389
349
  end
390
350
 
391
- # Return output data
392
- output_data
351
+ # Return output if you need it
352
+ output
393
353
  end
394
354
  end
355
+
356
+ # Use your handler (usually from an entrypoint file, but this is an example)
357
+ if __FILE__ == $0
358
+ # Read Claude Code's input data from STDIN
359
+ input_data = JSON.parse(STDIN.read)
360
+
361
+ hook = AddContextAfterPrompt.new(input_data)
362
+ # Call the hook
363
+ hook.call
364
+
365
+ # Uses exit code 0 (success) and outputs to STDIN if the prompt wasn't blocked
366
+ # Uses exit code 2 (blocking error) and outputs to STDERR if the prompt was blocked
367
+ hook.output_and_exit
368
+ end
395
369
  ```
396
370
 
371
+ ## 📚 API Reference
372
+
373
+ 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.
374
+
375
+ Each hook provides the following capabilities:
376
+
377
+ | Category | Description |
378
+ |----------|-------------|
379
+ | Configuration & Utility | Access config, logging, and file path helpers |
380
+ | Input Helpers | Access data parsed from STDIN (`session_id`, `transcript_path`, etc.) |
381
+ | Hook State Helpers | Modify the hook's internal state (adding additional context, blocking a tool call, etc...) before yielding back to Claude Code |
382
+ | Output Helpers | Access output data, merge results, and yield back to Claude with the proper exit codes |
383
+
397
384
  ### Input Fields
398
385
 
399
386
  The framework supports all existing hook types with their respective input fields:
@@ -409,209 +396,26 @@ The framework supports all existing hook types with their respective input field
409
396
  | **SubagentStop** | `stop_hook_active` |
410
397
  | **PreCompact** | `trigger`, `custom_instructions` |
411
398
  | **SessionStart** | `source` |
399
+ | **SessionEnd** | `reason` |
412
400
 
413
- ## 📚 API Reference
414
-
415
- The whole purpose of those APIs is to simplify reading from STDIN and writing to STDOUT the way Claude Code expects you to.
416
-
417
- ### Common API Methods
418
-
419
- Those methods are available in **all hook types** and are inherited from `ClaudeHooks::Base`:
420
-
421
- #### Input Methods
422
- Input methods are helpers to access data parsed from STDIN.
423
-
424
- | Method | Description |
425
- |--------|-------------|
426
- | `input_data` | Input data reader |
427
- | `session_id` | Get the current session ID |
428
- | `transcript_path` | Get path to the transcript file |
429
- | `cwd` | Get current working directory |
430
- | `hook_event_name` | Get the hook event name |
431
- | `read_transcript` | Read the transcript file |
432
- | `transcript` | Alias for `read_transcript` |
433
-
434
- #### Output Methods
435
- Output methods are helpers to modify `output_data`.
436
-
437
- | Method | Description |
438
- |--------|-------------|
439
- | `output_data` | Output data accessor |
440
- | `stringify_output` | Generates a JSON string from `output_data` |
441
- | `allow_continue!` | Allow Claude to continue (default) |
442
- | `prevent_continue!(reason)` | Stop Claude with reason |
443
- | `suppress_output!` | Hide stdout from transcript |
444
- | `show_output!` | Show stdout in transcript (default) |
445
- | `clear_specifics!` | Clear hook-specific output |
446
-
447
- #### Class Output Methods
448
-
449
- 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)`.
450
-
451
- | Method | Description |
452
- |--------|-------------|
453
- | `merge_outputs(*outputs_data)` | Intelligently merge multiple outputs into a single output |
454
-
455
- ### Configuration and Utility Methods
456
-
457
- Available in all hooks via the base `ClaudeHooks::Base` class:
458
-
459
- #### Utility Methods
460
- | Method | Description |
461
- |--------|-------------|
462
- | `log(message, level: :info)` | Log to session-specific file (levels: :info, :warn, :error) |
463
-
464
- #### Configuration Methods
465
- | Method | Description |
466
- |--------|-------------|
467
- | `home_claude_dir` | Get the home Claude directory (`$HOME/.claude`) |
468
- | `project_claude_dir` | Get the project Claude directory (`$CLAUDE_PROJECT_DIR/.claude`, or `nil`) |
469
- | `home_path_for(relative_path)` | Get absolute path relative to home Claude directory |
470
- | `project_path_for(relative_path)` | Get absolute path relative to project Claude directory (or `nil`) |
471
- | `base_dir` | Get the base Claude directory (**deprecated**) |
472
- | `path_for(relative_path, base_dir=nil)` | Get absolute path relative to specified or default base dir (**deprecated**) |
473
- | `config` | Access the merged configuration object |
474
- | `config.get_config_value(env_key, config_file_key, default)` | Get any config value with fallback |
475
- | `config.logs_directory` | Get logs directory path (always under home directory) |
476
- | `config.your_custom_key` | Access any custom config via method_missing |
477
-
478
-
479
- ### UserPromptSubmit API
480
-
481
- Available when inheriting from `ClaudeHooks::UserPromptSubmit`:
482
-
483
- #### Input Methods
484
- | Method | Description |
485
- |--------|-------------|
486
- | `prompt` | Get the user's prompt text |
487
- | `user_prompt` | Alias for `prompt` |
488
- | `current_prompt` | Alias for `prompt` |
489
-
490
- #### Output Methods
491
- | Method | Description |
492
- |--------|-------------|
493
- | `add_additional_context!(context)` | Add context to the prompt |
494
- | `add_context!(context)` | Alias for `add_additional_context!` |
495
- | `empty_additional_context!` | Remove additional context |
496
- | `block_prompt!(reason)` | Block the prompt from processing |
497
- | `unblock_prompt!` | Unblock a previously blocked prompt |
498
-
499
- ### PreToolUse API
500
-
501
- Available when inheriting from `ClaudeHooks::PreToolUse`:
502
-
503
- #### Input Methods
504
- | Method | Description |
505
- |--------|-------------|
506
- | `tool_name` | Get the name of the tool being used |
507
- | `tool_input` | Get the input data for the tool |
401
+ ### Hooks API
508
402
 
509
- #### Output Methods
510
- | Method | Description |
511
- |--------|-------------|
512
- | `approve_tool!(reason)` | Explicitly approve tool usage |
513
- | `block_tool!(reason)` | Block tool usage with feedback |
514
- | `ask_for_permission!(reason)` | Request user permission |
515
-
516
- ### PostToolUse API
517
-
518
- Available when inheriting from `ClaudeHooks::PostToolUse`:
519
-
520
- #### Input Methods
521
- | Method | Description |
522
- |--------|-------------|
523
- | `tool_name` | Get the name of the tool that was used |
524
- | `tool_input` | Get the input that was passed to the tool |
525
- | `tool_response` | Get the tool's response/output |
526
-
527
- #### Output Methods
528
- | Method | Description |
529
- |--------|-------------|
530
- | `block_tool!(reason)` | Block the tool result from being used |
531
- | `approve_tool!(reason)` | Clear any previous block decision (allows tool result) |
532
-
533
- ### Notification API
534
-
535
- Available when inheriting from `ClaudeHooks::Notification`:
536
-
537
- #### Input Methods
538
- | Method | Description |
539
- |--------|-------------|
540
- | `message` | Get the notification message content |
541
- | `notification_message` | Alias for `message` |
403
+ **All hook types** inherit from `ClaudeHooks::Base` and share a common API, as well as hook specific APIs.
542
404
 
543
- #### Output Methods
544
- Notifications are outside facing and do not have any specific output methods.
545
-
546
- ### Stop API
547
-
548
- Available when inheriting from `ClaudeHooks::Stop`:
549
-
550
- #### Input Methods
551
- | Method | Description |
552
- |--------|-------------|
553
- | `stop_hook_active` | Check if Claude Code is already continuing as a result of a stop hook |
554
-
555
- #### Output Methods
556
- | Method | Description |
557
- |--------|-------------|
558
- | `continue_with_instructions!(instructions)` | Block Claude from stopping and provide instructions to continue |
559
- | `block!(instructions)` | Alias for `continue_with_instructions!` |
560
- | `ensure_stopping!` | Allow Claude to stop normally (default behavior) |
561
-
562
- ### SubagentStop API
563
-
564
- Available when inheriting from `ClaudeHooks::SubagentStop` (inherits from `ClaudeHooks::Stop`):
565
-
566
- #### Input Methods
567
- | Method | Description |
568
- |--------|-------------|
569
- | `stop_hook_active` | Check if Claude Code is already continuing as a result of a stop hook |
570
-
571
- #### Output Methods
572
- | Method | Description |
573
- |--------|-------------|
574
- | `continue_with_instructions!(instructions)` | Block Claude from stopping and provide instructions to continue |
575
- | `block!(instructions)` | Alias for `continue_with_instructions!` |
576
- | `ensure_stopping!` | Allow Claude to stop normally (default behavior) |
577
-
578
- ### PreCompact API
579
-
580
- Available when inheriting from `ClaudeHooks::PreCompact`:
581
-
582
- #### Input Methods
583
- | Method | Description |
584
- |--------|-------------|
585
- | `trigger` | Get the compaction trigger: `'manual'` or `'auto'` |
586
- | `custom_instructions` | Get custom instructions (only available for manual trigger) |
587
-
588
- #### Output Methods
589
- No specific output methods are available to alter compaction behavior.
590
-
591
- #### Utility Methods
592
- | Method | Description |
593
- |--------|-------------|
594
- | `backup_transcript!(backup_file_path)` | Create a backup of the transcript at the specified path |
595
-
596
- ### SessionStart API
597
-
598
- Available when inheriting from `ClaudeHooks::SessionStart`:
599
-
600
- #### Input Methods
601
- | Method | Description |
602
- |--------|-------------|
603
- | `source` | Get the session start source: `'startup'`, `'resume'`, or `'clear'` |
604
-
605
- #### Output Methods
606
- | Method | Description |
607
- |--------|-------------|
608
- | `add_additional_context!(context)` | Add contextual information for Claude's session |
609
- | `add_context!(context)` | Alias for `add_additional_context!` |
610
- | `empty_additional_context!` | Clear additional context |
405
+ - [📚 Common API Methods](docs/API/COMMON.md)
406
+ - [🔔 Notification Hooks](docs/API/NOTIFICATION.md)
407
+ - [🚀 Session Start Hooks](docs/API/SESSION_START.md)
408
+ - [🖋️ User Prompt Submit Hooks](docs/API/USER_PROMPT_SUBMIT.md)
409
+ - [🛠️ Pre-Tool Use Hooks](docs/API/PRE_TOOL_USE.md)
410
+ - [🔧 Post-Tool Use Hooks](docs/API/POST_TOOL_USE.md)
411
+ - [📝 Pre-Compact Hooks](docs/API/PRE_COMPACT.md)
412
+ - [⏹️ Stop Hooks](docs/API/STOP.md)
413
+ - [⏹️ Subagent Stop Hooks](docs/API/SUBAGENT_STOP.md)
414
+ - [🔚 Session End Hooks](docs/API/SESSION_END.md)
611
415
 
612
416
  ### 📝 Logging
613
417
 
614
- `ClaudeHooks::Base` provides a **session logger** that will write logs to session-specific files.
418
+ `ClaudeHooks::Base` provides a **session logger** to all its subclasses that you can use to write logs to session-specific files.
615
419
 
616
420
  ```ruby
617
421
  log "Simple message"
@@ -629,7 +433,8 @@ You can also use the logger from an entrypoint script:
629
433
  ```ruby
630
434
  require 'claude_hooks'
631
435
 
632
- logger = ClaudeHooks::Logger.new("TEST-SESSION-01", 'entrypoint')
436
+ input_data = JSON.parse(STDIN.read)
437
+ logger = ClaudeHooks::Logger.new(input_data["session_id"], 'entrypoint')
633
438
  logger.log "Simple message"
634
439
  ```
635
440
 
@@ -642,6 +447,7 @@ Logs are written to session-specific files in the configured log directory:
642
447
  ```
643
448
  [2025-08-16 03:45:28] [INFO] [MyHookHandler] Starting execution
644
449
  [2025-08-16 03:45:28] [ERROR] [MyHookHandler] Connection timeout
450
+ ...
645
451
  ```
646
452
 
647
453
  ## 📝 Example: Tool usage monitor
@@ -666,7 +472,7 @@ First, register an entrypoint in `~/.claude/settings.json`:
666
472
  }
667
473
  ```
668
474
 
669
- Then, create your main entrypoint script and don't forget to make it executable:
475
+ Then, create your main entrypoint script and _don't forget to make it executable_:
670
476
  ```bash
671
477
  touch ~/.claude/hooks/entrypoints/pre_tool_use.rb
672
478
  chmod +x ~/.claude/hooks/entrypoints/pre_tool_use.rb
@@ -683,28 +489,19 @@ begin
683
489
  input_data = JSON.parse(STDIN.read)
684
490
 
685
491
  tool_monitor = ToolMonitor.new(input_data)
686
- output = tool_monitor.call
492
+ tool_monitor.call
687
493
 
688
- # Any other hook scripts can be chained here
494
+ # You could also call any other handler here and then merge the outputs
689
495
 
690
- puts JSON.generate(output)
691
-
692
- rescue JSON::ParserError => e
693
- log "Error parsing JSON: #{e.message}", level: :error
694
- puts JSON.generate({
695
- continue: false,
696
- stopReason: "JSON parsing error: #{e.message}",
697
- suppressOutput: false
698
- })
699
- exit 0
496
+ tool_monitor.output_and_exit
700
497
  rescue StandardError => e
701
- log "Error in ToolMonitor hook: #{e.message}", level: :error
702
- puts JSON.generate({
498
+ STDERR.puts JSON.generate({
703
499
  continue: false,
704
500
  stopReason: "Hook execution error: #{e.message}",
705
501
  suppressOutput: false
706
502
  })
707
- exit 0
503
+ # Non-blocking error
504
+ exit 1
708
505
  end
709
506
  ```
710
507
 
@@ -727,51 +524,107 @@ class ToolMonitor < ClaudeHooks::PreToolUse
727
524
 
728
525
  if DANGEROUS_TOOLS.include?(tool_name)
729
526
  log "Dangerous tool detected: #{tool_name}", level: :warn
527
+ # Use one of the ClaudeHooks::PreToolUse methods to modify the hook state and block the tool
730
528
  ask_for_permission!("The tool '#{tool_name}' can impact your system. Allow?")
731
529
  else
530
+ # Use one of the ClaudeHooks::PreToolUse methods to modify the hook state and allow the tool
732
531
  approve_tool!("Safe tool usage")
733
532
  end
734
533
 
735
- output_data
534
+ # Accessor provided by ClaudeHooks::PreToolUse
535
+ output
736
536
  end
737
537
  end
738
538
  ```
739
539
 
740
540
  ## 🔄 Hook Output
741
541
 
542
+ Hooks provide access to their output (which acts as the "state" of a hook) through the `output` method.
543
+
544
+ This method will return an output object based on the hook's type class (e.g: `ClaudeHooks::Output::UserPromptSubmit`) that provides helper methods:
545
+ - to access output data
546
+ - for merging multiple outputs
547
+ - for sending the right exit codes and output data back to Claude Code through the proper stream.
548
+
549
+ > [!TIP]
550
+ > You can also always access the raw output data hash instead of the output object using `hook.output_data`.
551
+
552
+
742
553
  ### 🔄 Hook Output Merging
743
554
 
744
- Each hook script type provides a merging method `merge_outputs` that will try to intelligently merge multiple hook results:
555
+ Often, you will want to call multiple hooks from a same entrypoint.
556
+ Each hook type's `output` provides a `merge` method that will try to intelligently merge multiple hook results.
557
+ Merged outputs always inherit the **most restrictive behavior**.
745
558
 
746
559
  ```ruby
747
- # Merge results from multiple UserPromptSubmit hooks
748
- merged_result = ClaudeHooks::UserPromptSubmit.merge_outputs(output1, output2, output3)
749
560
 
750
- # ClaudeHooks::UserPromptSubmit.merge_outputs follows the following merge logic:
751
- # - continue: false wins (any hook script can stop execution)
752
- # - suppressOutput: true wins (any hook script can suppress output)
753
- # - decision: "block" wins (any hook script can block)
754
- # - stopReason/reason: concatenated
755
- # - additionalContext: joined
561
+ require 'json'
562
+ require_relative '../handlers/user_prompt_submit/hook1'
563
+ require_relative '../handlers/user_prompt_submit/hook2'
564
+ require_relative '../handlers/user_prompt_submit/hook3'
565
+
566
+ begin
567
+ # Read input from stdin
568
+ input_data = JSON.parse(STDIN.read)
569
+
570
+ hook1 = Hook1.new(input_data)
571
+ hook2 = Hook1.new(input_data)
572
+ hook3 = Hook1.new(input_data)
573
+
574
+ # Execute the multiple hooks
575
+ hook1.call
576
+ hook2.call
577
+ hook3.call
578
+
579
+ # Merge the outputs
580
+ # In this case, ClaudeHooks::Output::UserPromptSubmit.merge follows the following merge logic:
581
+ # - continue: false wins (any hook script can stop execution)
582
+ # - suppressOutput: true wins (any hook script can suppress output)
583
+ # - decision: "block" wins (any hook script can block)
584
+ # - stopReason/reason: concatenated
585
+ # - additionalContext: concatenated
586
+ merged_output = ClaudeHooks::Output::UserPromptSubmit.merge(
587
+ hook1.output,
588
+ hook2.output,
589
+ hook3.output
590
+ )
591
+
592
+ # Automatically handles outputting to the right stream (STDOUT or STDERR) and uses the right exit code depending on hook state
593
+ merged_output.output_and_exit
594
+ end
756
595
  ```
757
596
 
758
597
  ### 🚪 Hook Exit Codes
759
598
 
760
- Claude Code hooks support multiple exit codes:
599
+ > [!NOTE]
600
+ > Hooks and output objects handle exit codes automatically. The information below is for reference and understanding. When using `hook.output_and_exit` or `merged_output.output_and_exit`, you don't need to memorize these rules - the method chooses the correct exit code based on the hook type and the hook's state.
601
+
602
+ Claude Code hooks support multiple exit codes with different behaviors depending on the hook type.
761
603
 
762
- ### Pattern 1: Simple Exit Codes
763
- - **`exit 0`**: Success, allow the operation to continue
764
- - **`exit 1`**: Non-blocking error, `STDERR` will be fed back to the user
765
- - **`exit 2`**: Blocking error, `STDERR` will be fed back to Claude
604
+ - **`exit 0`**: Success, allows the operation to continue, for most hooks, `STDOUT` will be fed back to the user.
605
+ - Claude Code does not see stdout if the exit code is 0, except for hooks where `STDOUT` is injected as context.
606
+ - **`exit 1`**: Non-blocking error, `STDERR` will be fed back to the user.
607
+ - **`exit 2`**: Blocking error, in most cases `STDERR` will be fed back to Claude.
608
+ - **Other exit codes**: Treated as non-blocking errors - `STDERR` fed back to the user, execution continues.
766
609
 
767
- **Hook-specific meanings:**
768
- - **UserPromptSubmit**: `exit 1` blocks the prompt from being processed
769
- - **PreToolUse**: `exit 1` blocks the tool, `exit 2` asks for permission
770
- - **PostToolUse**: `exit 1` blocks the tool result from being used
610
+ > [!WARNING]
611
+ > Some exit codes have different meanings depending on the hook type, here is a table to help summarize this.
612
+
613
+ | Hook Event | Exit 0 (Success) | Exit 1 (Non-blocking Error) | Exit Code 2 (Blocking Error) |
614
+ |------------------|------------------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------|
615
+ | UserPromptSubmit | Operation continues<br/><br />**`STDOUT` added as context to Claude** | Non-blocking error<br/><br />`STDERR` shown to user | **Blocks prompt processing**<br/>**Erases prompt**<br/><br />`STDERR` shown to user only |
616
+ | PreToolUse | Operation continues<br/><br />`STDOUT` shown to user in transcript mode | Non-blocking error<br/><br />`STDERR` shown to user | **Blocks the tool call**<br/><br />`STDERR` shown to Claude |
617
+ | PostToolUse | Operation continues<br/><br />`STDOUT` shown to user in transcript mode | Non-blocking error<br/><br />`STDERR` shown to user | N/A<br/><br />`STDERR` shown to Claude *(tool already ran)* |
618
+ | Notification | 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`) |
619
+ | Stop | Agent will stop<br/><br />`STDOUT` shown to user in transcript mode | Agent will stop<br/><br />`STDERR` shown to user | **Blocks stoppage**<br/><br />`STDERR` shown to Claude |
620
+ | SubagentStop | Subagent will stop<br/><br />`STDOUT` shown to user in transcript mode | Subagent will stop<br/><br />`STDERR` shown to user | **Blocks stoppage**<br/><br />`STDERR` shown to Claude subagent |
621
+ | PreCompact | Operation continues<br/><br />`STDOUT` shown to user in transcript mode | Non-blocking error<br/><br />`STDERR` shown to user | N/A<br/><br />`STDERR` shown to user only |
622
+ | 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 |
623
+ | 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`) |
771
624
 
772
625
 
773
- ### Example: Success
774
- For the operation to continue for a UserPromptSubmit hook, you would return structured JSON data followed by `exit 0`:
626
+ #### Manually outputing and exiting example with success
627
+ For the operation to continue for a `UserPromptSubmit` hook, you would `STDOUT.puts` structured JSON data followed by `exit 0`:
775
628
 
776
629
  ```ruby
777
630
  puts JSON.generate({
@@ -786,9 +639,8 @@ puts JSON.generate({
786
639
  exit 0
787
640
  ```
788
641
 
789
- ### Example: Error
790
-
791
- For the operation to stop for a UserPromptSubmit hook, you would return structured JSON data followed by `exit 1`:
642
+ #### Manually outputing and exiting example with error
643
+ For the operation to stop for a `UserPromptSubmit` hook, you would `STDERR.puts` structured JSON data followed by `exit 2`:
792
644
 
793
645
  ```ruby
794
646
  STDERR.puts JSON.generate({
@@ -796,19 +648,84 @@ STDERR.puts JSON.generate({
796
648
  stopReason: "JSON parsing error: #{e.message}",
797
649
  suppressOutput: false
798
650
  })
799
- exit 1
651
+ exit 2
800
652
  ```
801
653
 
802
654
  > [!WARNING]
803
- > Don't forget to use `STDERR.puts` to output the JSON to STDERR.
655
+ > You don't have to manually do this, just use `output_and_exit` to automatically handle this.
656
+
657
+ ## 🔌 Plugin Hooks Support
804
658
 
659
+ This DSL works seamlessly with [Claude Code plugins](https://docs.claude.com/en/docs/claude-code/plugins)! When creating plugin hooks, you can use the exact same Ruby DSL and enjoy all the same benefits.
660
+
661
+ **How plugin hooks work:**
662
+ - Plugin hooks are defined in the plugin's `hooks/hooks.json` file
663
+ - They use the `${CLAUDE_PLUGIN_ROOT}` environment variable to reference plugin files
664
+ - Plugin hooks are automatically merged with user and project hooks when plugins are enabled
665
+ - Multiple hooks from different sources can respond to the same event
666
+
667
+ **Example plugin hook configuration (`hooks/hooks.json`):**
668
+ ```json
669
+ {
670
+ "description": "Automatic code formatting",
671
+ "hooks": {
672
+ "PostToolUse": [
673
+ {
674
+ "matcher": "Write|Edit",
675
+ "hooks": [
676
+ {
677
+ "type": "command",
678
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/formatter.rb",
679
+ "timeout": 30
680
+ }
681
+ ]
682
+ }
683
+ ]
684
+ }
685
+ }
686
+ ```
687
+
688
+ **Using this DSL in your plugin hooks (`hooks/scripts/formatter.rb`):**
689
+ ```ruby
690
+ #!/usr/bin/env ruby
691
+ require 'claude_hooks'
692
+
693
+ class PluginFormatter < ClaudeHooks::PostToolUse
694
+ def call
695
+ log "Plugin executing from: #{ENV['CLAUDE_PLUGIN_ROOT']}"
696
+
697
+ if tool_name.match?(/Write|Edit/)
698
+ file_path = tool_input['file_path']
699
+ log "Formatting file: #{file_path}"
700
+
701
+ # Your formatting logic here
702
+ # Can use all the DSL helper methods!
703
+ end
704
+
705
+ output
706
+ end
707
+ end
708
+
709
+ if __FILE__ == $0
710
+ input_data = JSON.parse(STDIN.read)
711
+ hook = PluginFormatter.new(input_data)
712
+ hook.call
713
+ hook.output_and_exit
714
+ end
715
+ ```
716
+
717
+ **Environment variables available in plugins:**
718
+ - `${CLAUDE_PLUGIN_ROOT}`: Absolute path to the plugin directory
719
+ - `${CLAUDE_PROJECT_DIR}`: Project root directory (same as for project hooks)
720
+ - All standard environment variables and configuration options work the same way
721
+
722
+ See the [plugin components reference](https://docs.claude.com/en/docs/claude-code/plugins-reference#hooks) for more details on creating plugin hooks.
805
723
 
806
724
  ## 🚨 Advices
807
725
 
808
- 1. **Logging**: Use `log()` method instead of `puts` to avoid interfering with JSON output
726
+ 1. **Logging**: Use `log()` method instead of `puts` to avoid interfering with Claude Code's expected output.
809
727
  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`.
810
- 3. **Output Format**: Always return `output_data` or `nil` from your `call` method
811
- 4. **Path Management**: Use `path_for()` for all file operations relative to the Claude base directory
728
+ 3. **Path Management**: Use `path_for()` for all file operations relative to the Claude base directory.
812
729
 
813
730
  ## ⚠️ Troubleshooting
814
731
 
@@ -911,7 +828,7 @@ class MyTestHook < ClaudeHooks::UserPromptSubmit
911
828
  log "All input keys: #{input_data.keys.join(', ')}"
912
829
  end
913
830
 
914
- output_data
831
+ output
915
832
  end
916
833
  end
917
834
 
@@ -929,5 +846,19 @@ end
929
846
 
930
847
  ```bash
931
848
  # Test with sample data
932
- 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
849
+ 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/entrypoints/user_prompt_submit.rb
850
+ ```
851
+
852
+ ## 🧪 Development & Contributing
853
+
854
+ ### Running Tests
855
+
856
+ This project uses Minitest for testing. To run the complete test suite:
857
+
858
+ ```bash
859
+ # Run all tests
860
+ ruby test/run_all_tests.rb
861
+
862
+ # Run a specific test file
863
+ ruby test/test_output_classes.rb
933
864
  ```