claude_hooks 0.2.0 → 1.0.0

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