claude_hooks 1.0.0 → 1.1.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/auto-fix.md +7 -0
  3. data/CHANGELOG.md +99 -4
  4. data/README.md +91 -15
  5. data/docs/{OUTPUT_MIGRATION_GUIDE.md → 1.0.0_MIGRATION_GUIDE.md} +17 -17
  6. data/docs/1.1.0_UPGRADE_GUIDE.md +269 -0
  7. data/docs/API/COMMON.md +1 -0
  8. data/docs/API/NOTIFICATION.md +1 -0
  9. data/docs/API/PERMISSION_REQUEST.md +196 -0
  10. data/docs/API/POST_TOOL_USE.md +1 -0
  11. data/docs/API/PRE_TOOL_USE.md +4 -0
  12. data/docs/API/SESSION_START.md +1 -1
  13. data/docs/WHY.md +15 -8
  14. data/docs/external/claude-hooks-reference.md +1310 -0
  15. data/example_dotclaude/hooks/entrypoints/pre_tool_use.rb +25 -0
  16. data/example_dotclaude/hooks/handlers/pre_tool_use/github_guard.rb +253 -0
  17. data/example_dotclaude/hooks/handlers/user_prompt_submit/append_rules.rb +2 -1
  18. data/example_dotclaude/plugins/README.md +175 -0
  19. data/example_dotclaude/settings.json +11 -0
  20. data/lib/claude_hooks/base.rb +5 -1
  21. data/lib/claude_hooks/notification.rb +5 -1
  22. data/lib/claude_hooks/output/base.rb +2 -0
  23. data/lib/claude_hooks/output/permission_request.rb +95 -0
  24. data/lib/claude_hooks/output/post_tool_use.rb +14 -6
  25. data/lib/claude_hooks/output/pre_tool_use.rb +47 -32
  26. data/lib/claude_hooks/output/stop.rb +13 -6
  27. data/lib/claude_hooks/output/user_prompt_submit.rb +14 -7
  28. data/lib/claude_hooks/permission_request.rb +56 -0
  29. data/lib/claude_hooks/post_tool_use.rb +5 -1
  30. data/lib/claude_hooks/pre_tool_use.rb +16 -1
  31. data/lib/claude_hooks/stop.rb +8 -0
  32. data/lib/claude_hooks/version.rb +1 -1
  33. data/lib/claude_hooks.rb +2 -0
  34. metadata +12 -3
@@ -0,0 +1,269 @@
1
+ # Upgrading to v1.1.0
2
+
3
+ Version 1.1.0 adds new features to align with Anthropic's updated Claude Code Hooks documentation. **All changes are backward compatible** - existing hooks continue to work without modification.
4
+
5
+ ## What's New
6
+
7
+ ### 1. New PermissionRequest Hook
8
+
9
+ Handle permission request events from Claude Code:
10
+
11
+ ```ruby
12
+ class PermissionGuard < ClaudeHooks::PermissionRequest
13
+ def call
14
+ if dangerous_tool?
15
+ deny_permission!("Requires manual approval")
16
+ else
17
+ allow_permission!("Safe to proceed")
18
+ end
19
+ output
20
+ end
21
+
22
+ private
23
+
24
+ def dangerous_tool?
25
+ %w[rm git-reset curl].include?(tool_name)
26
+ end
27
+ end
28
+ ```
29
+
30
+ **When to use**: Create permission guards that automatically approve/deny permission requests based on custom logic.
31
+
32
+ ### 2. New Fields Available
33
+
34
+ All hooks now receive these additional fields:
35
+
36
+ - **`permission_mode`** (all hooks): Access via `permission_mode` method
37
+ - Values: `'default'`, `'plan'`, `'acceptEdits'`, `'dontAsk'`, `'bypassPermissions'`
38
+ - Defaults to `'default'`
39
+
40
+ - **`tool_use_id`** (PreToolUse, PostToolUse, PermissionRequest): Access via `tool_use_id` method
41
+ - Unique identifier for each tool use (e.g., `"toolu_01ABC123..."`)
42
+
43
+ - **`notification_type`** (Notification): Access via `notification_type` method
44
+ - Values: `'permission_prompt'`, `'idle_prompt'`, `'auth_success'`, `'elicitation_dialog'`
45
+
46
+ **Example usage:**
47
+
48
+ ```ruby
49
+ class MyHook < ClaudeHooks::PreToolUse
50
+ def call
51
+ log "Permission mode: #{permission_mode}"
52
+ log "Tool use ID: #{tool_use_id}"
53
+
54
+ # Adjust behavior based on permission mode
55
+ if permission_mode == 'bypassPermissions'
56
+ # More restrictive checks
57
+ deny_permission!("Cannot bypass for sensitive operations")
58
+ else
59
+ approve_tool!("Approved")
60
+ end
61
+
62
+ output
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### 3. Update Tool Input (PreToolUse)
68
+
69
+ Modify tool input before execution:
70
+
71
+ ```ruby
72
+ class InputModifier < ClaudeHooks::PreToolUse
73
+ def call
74
+ case tool_name
75
+ when 'Bash'
76
+ # Add safety flags to bash commands
77
+ if tool_input['command']&.include?('rm')
78
+ sanitized = tool_input.merge('command' => "#{tool_input['command']} --interactive")
79
+ update_tool_input!(sanitized)
80
+ else
81
+ approve_tool!("Safe command")
82
+ end
83
+ when 'Write'
84
+ # Validate file paths
85
+ if tool_input['file_path']&.start_with?('/etc')
86
+ deny_permission!("Cannot write to /etc")
87
+ else
88
+ approve_tool!("Safe write")
89
+ end
90
+ else
91
+ approve_tool!("No special handling")
92
+ end
93
+
94
+ output
95
+ end
96
+ end
97
+ ```
98
+
99
+ **Output helpers:**
100
+ - `output.input_updated?` - Check if input was modified
101
+ - `output.updated_input` - Get the updated input hash
102
+
103
+ ## Do I Need to Change My Existing Hooks?
104
+
105
+ **No!** All changes are backward compatible:
106
+
107
+ - New fields have sensible defaults (return `nil` or `'default'`)
108
+ - Existing methods continue to work exactly as before
109
+ - No breaking changes to any APIs
110
+ - Your existing hooks will work with v1.1.0 without any modifications
111
+
112
+ ## Should I Update My Hooks?
113
+
114
+ Consider updating if:
115
+
116
+ - ✅ You want to handle PermissionRequest events
117
+ - ✅ You need to check the current permission mode
118
+ - ✅ You want to modify tool inputs dynamically
119
+ - ✅ You need to differentiate notification types
120
+ - ✅ You want to track specific tool use instances via IDs
121
+
122
+ ## New Hook Type Summary
123
+
124
+ | Hook Type | When It Runs | Key Methods |
125
+ |-----------|--------------|-------------|
126
+ | **PermissionRequest** | When Claude requests permission | `allow_permission!`, `deny_permission!`, `update_input_and_allow!` |
127
+
128
+ ## New Fields Summary
129
+
130
+ | Field | Available In | Purpose |
131
+ |-------|--------------|---------|
132
+ | `permission_mode` | All hooks | Know the current permission mode |
133
+ | `tool_use_id` | PreToolUse, PermissionRequest, PostToolUse | Track specific tool use instances |
134
+ | `notification_type` | Notification | Filter notifications by type |
135
+
136
+ ## New PreToolUse Features
137
+
138
+ | Feature | Method | Description |
139
+ |---------|--------|-------------|
140
+ | Update input | `update_tool_input!(hash)` | Modify tool input and auto-approve |
141
+ | Check if updated | `output.input_updated?` | Boolean check for modification |
142
+ | Get updated input | `output.updated_input` | Retrieve the modified input |
143
+
144
+ ## Testing
145
+
146
+ All existing tests should pass without modification. Run:
147
+
148
+ ```bash
149
+ bundle exec rake test
150
+ # or
151
+ ruby test/run_all_tests.rb
152
+ ```
153
+
154
+ ## Migration Examples
155
+
156
+ ### Example 1: Using permission_mode
157
+
158
+ **Before (v1.0.x):**
159
+ ```ruby
160
+ class GitGuard < ClaudeHooks::PreToolUse
161
+ def call
162
+ if tool_name == 'Bash' && tool_input['command']&.include?('git push')
163
+ ask_for_permission!("Git push requires confirmation")
164
+ else
165
+ approve_tool!("Approved")
166
+ end
167
+ output
168
+ end
169
+ end
170
+ ```
171
+
172
+ **After (v1.1.0 - enhanced):**
173
+ ```ruby
174
+ class GitGuard < ClaudeHooks::PreToolUse
175
+ def call
176
+ if tool_name == 'Bash' && tool_input['command']&.include?('git push')
177
+ # Skip asking in dontAsk mode, but still log
178
+ if permission_mode == 'dontAsk'
179
+ log "Auto-approving git push in dontAsk mode", level: :warn
180
+ approve_tool!("Auto-approved due to permission mode")
181
+ else
182
+ ask_for_permission!("Git push requires confirmation")
183
+ end
184
+ else
185
+ approve_tool!("Approved")
186
+ end
187
+ output
188
+ end
189
+ end
190
+ ```
191
+
192
+ ### Example 2: Using PermissionRequest
193
+
194
+ **Before (v1.0.x):**
195
+ ```ruby
196
+ # Permission requests couldn't be handled automatically
197
+ ```
198
+
199
+ **After (v1.1.0 - new capability):**
200
+ ```ruby
201
+ class AutoPermissionGuard < ClaudeHooks::PermissionRequest
202
+ SAFE_TOOLS = %w[ls cat grep find read].freeze
203
+
204
+ def call
205
+ if SAFE_TOOLS.include?(tool_name)
206
+ allow_permission!("Safe tool, auto-approved")
207
+ else
208
+ # Let user decide
209
+ deny_permission!("Requires manual approval")
210
+ end
211
+ output
212
+ end
213
+ end
214
+ ```
215
+
216
+ ### Example 3: Updating tool input
217
+
218
+ **Before (v1.0.x):**
219
+ ```ruby
220
+ class BashSanitizer < ClaudeHooks::PreToolUse
221
+ def call
222
+ # Could only approve or deny, not modify
223
+ if tool_input['command']&.include?('rm')
224
+ block_tool!("Dangerous command blocked")
225
+ else
226
+ approve_tool!("Safe")
227
+ end
228
+ output
229
+ end
230
+ end
231
+ ```
232
+
233
+ **After (v1.1.0 - new capability):**
234
+ ```ruby
235
+ class BashSanitizer < ClaudeHooks::PreToolUse
236
+ def call
237
+ if tool_input['command']&.include?('rm')
238
+ # Add safety flags instead of blocking
239
+ safe_command = "#{tool_input['command']} --interactive --verbose"
240
+ update_tool_input!({'command' => safe_command})
241
+ log "Added safety flags to rm command"
242
+ else
243
+ approve_tool!("Safe")
244
+ end
245
+ output
246
+ end
247
+ end
248
+ ```
249
+
250
+ ## Questions?
251
+
252
+ - See the [full CHANGELOG](../CHANGELOG.md) for all changes
253
+ - Check the [API documentation](API/) for detailed method references
254
+ - Review the [PermissionRequest API](API/PERMISSION_REQUEST.md) for the new hook type
255
+ - Look at [example implementations](../example_dotclaude/) for patterns
256
+
257
+ ## Rollback
258
+
259
+ If you need to rollback to v1.0.2:
260
+
261
+ ```bash
262
+ gem uninstall claude_hooks
263
+ gem install claude_hooks -v 1.0.2
264
+ ```
265
+
266
+ Or in your Gemfile:
267
+ ```ruby
268
+ gem 'claude_hooks', '~> 1.0.2'
269
+ ```
data/docs/API/COMMON.md CHANGED
@@ -12,6 +12,7 @@ Input helpers to access the data provided by Claude Code through `STDIN`.
12
12
  | `transcript_path` | Get path to the transcript file |
13
13
  | `cwd` | Get current working directory |
14
14
  | `hook_event_name` | Get the hook event name |
15
+ | `permission_mode` | Get the permission mode: `'default'`, `'plan'`, `'acceptEdits'`, `'dontAsk'`, `'bypassPermissions'` (defaults to `'default'`) |
15
16
  | `read_transcript` | Read the transcript file |
16
17
  | `transcript` | Alias for `read_transcript` |
17
18
 
@@ -11,6 +11,7 @@ Input helpers to access the data provided by Claude Code through `STDIN`.
11
11
  |--------|-------------|
12
12
  | `message` | Get the notification message content |
13
13
  | `notification_message` | Alias for `message` |
14
+ | `notification_type` | Get the notification type: `'permission_prompt'`, `'idle_prompt'`, `'auth_success'`, `'elicitation_dialog'` |
14
15
 
15
16
  ## Hook State Helpers
16
17
  Notifications are outside facing and do not have any specific state to modify.
@@ -0,0 +1,196 @@
1
+ # PermissionRequest API
2
+
3
+ Available when inheriting from `ClaudeHooks::PermissionRequest`:
4
+
5
+ ## Input Helpers
6
+ Input helpers to access the data provided by Claude Code through `STDIN`.
7
+
8
+ [📚 Shared input helpers](COMMON.md#input-helpers)
9
+
10
+ | Method | Description |
11
+ |--------|-------------|
12
+ | `tool_name` | Get the name of the tool requiring permission |
13
+ | `tool_input` | Get the input data for the tool |
14
+ | `tool_use_id` | Get the unique identifier for this tool use |
15
+
16
+ ## Hook State Helpers
17
+ Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
18
+
19
+ [📚 Shared hook state methods](COMMON.md#hook-state-methods)
20
+
21
+ | Method | Description |
22
+ |--------|-------------|
23
+ | `allow_permission!(reason = '')` | Allow the permission request with optional reason |
24
+ | `deny_permission!(reason = '')` | Deny the permission request with reason |
25
+ | `update_input_and_allow!(updated_input, reason = '')` | Update tool input and allow (convenience method) |
26
+
27
+ ## Output Helpers
28
+ Output helpers provide access to the hook's output data and helper methods for working with the output state.
29
+
30
+ [📚 Shared output helpers](COMMON.md#output-helpers)
31
+
32
+ | Method | Description |
33
+ |--------|-------------|
34
+ | `output.allowed?` | Check if permission has been allowed |
35
+ | `output.denied?` | Check if permission has been denied |
36
+ | `output.input_updated?` | Check if tool input has been updated |
37
+ | `output.permission_decision` | Get the permission decision: 'allow' or 'deny' |
38
+ | `output.permission_reason` | Get the reason for the permission decision |
39
+ | `output.updated_input` | Get the updated input (if provided) |
40
+
41
+ ## Hook Exit Codes
42
+
43
+ PermissionRequest hooks use the JSON API with exit code 0 for all permission decisions.
44
+
45
+ | Exit Code | Behavior |
46
+ |-----------|----------|
47
+ | `exit 0` | Permission decision processed<br/>`STDOUT` contains JSON with decision |
48
+ | `exit 1` | Non-blocking error<br/>`STDERR` shown to user |
49
+ | `exit 2` | **Not recommended for PermissionRequest**<br/>Use JSON API with exit 0 instead |
50
+
51
+ ## Example: Basic Permission Guard
52
+
53
+ ```ruby
54
+ #!/usr/bin/env ruby
55
+ require 'claude_hooks'
56
+
57
+ class PermissionGuard < ClaudeHooks::PermissionRequest
58
+ SENSITIVE_TOOLS = %w[rm git-push curl wget].freeze
59
+ SAFE_TOOLS = %w[ls cat grep find].freeze
60
+
61
+ def call
62
+ log "Permission requested for: #{tool_name}"
63
+ log "Tool use ID: #{tool_use_id}"
64
+ log "Permission mode: #{permission_mode}"
65
+
66
+ # Auto-allow safe tools
67
+ if SAFE_TOOLS.include?(tool_name)
68
+ allow_permission!("Safe tool, automatically allowed")
69
+ return output
70
+ end
71
+
72
+ # Block dangerous tools
73
+ if SENSITIVE_TOOLS.include?(tool_name)
74
+ deny_permission!("Dangerous tool #{tool_name} requires manual approval")
75
+ return output
76
+ end
77
+
78
+ # Default: allow with logging
79
+ allow_permission!("Tool allowed by default")
80
+ output
81
+ end
82
+ end
83
+
84
+ if __FILE__ == $PROGRAM_NAME
85
+ input_data = JSON.parse(STDIN.read)
86
+ hook = PermissionGuard.new(input_data)
87
+ hook.call
88
+ hook.output_and_exit
89
+ end
90
+ ```
91
+
92
+ ## Example: Permission Mode-Aware Guard
93
+
94
+ ```ruby
95
+ #!/usr/bin/env ruby
96
+ require 'claude_hooks'
97
+
98
+ class SmartPermissionGuard < ClaudeHooks::PermissionRequest
99
+ DANGEROUS_TOOLS = %w[rm git-reset curl wget].freeze
100
+
101
+ def call
102
+ log "Permission mode: #{permission_mode}"
103
+
104
+ # Block dangerous tools in bypass mode
105
+ if permission_mode == 'bypassPermissions' && dangerous_tool?
106
+ log "Blocking dangerous tool in bypass mode", level: :warn
107
+ deny_permission!("Cannot bypass permissions for dangerous tool: #{tool_name}")
108
+ return output
109
+ end
110
+
111
+ # Allow in 'dontAsk' mode if tool is not dangerous
112
+ if permission_mode == 'dontAsk' && !dangerous_tool?
113
+ allow_permission!("Auto-allowed in dontAsk mode")
114
+ return output
115
+ end
116
+
117
+ # Default behavior
118
+ allow_permission!("Permission granted")
119
+ output
120
+ end
121
+
122
+ private
123
+
124
+ def dangerous_tool?
125
+ DANGEROUS_TOOLS.include?(tool_name)
126
+ end
127
+ end
128
+ ```
129
+
130
+ ## Example: Input Modification
131
+
132
+ ```ruby
133
+ #!/usr/bin/env ruby
134
+ require 'claude_hooks'
135
+
136
+ class InputSanitizer < ClaudeHooks::PermissionRequest
137
+ def call
138
+ case tool_name
139
+ when 'Bash'
140
+ # Add safety flags to bash commands
141
+ if tool_input['command']&.include?('rm')
142
+ sanitized_input = tool_input.merge(
143
+ 'command' => tool_input['command'] + ' --interactive'
144
+ )
145
+ update_input_and_allow!(sanitized_input, "Added --interactive flag for safety")
146
+ else
147
+ allow_permission!("Bash command is safe")
148
+ end
149
+ when 'Write'
150
+ # Validate file paths before writing
151
+ if tool_input['file_path']&.start_with?('/tmp')
152
+ deny_permission!("Cannot write to /tmp directory")
153
+ else
154
+ allow_permission!("File write is safe")
155
+ end
156
+ else
157
+ allow_permission!("No special handling needed")
158
+ end
159
+
160
+ output
161
+ end
162
+ end
163
+ ```
164
+
165
+ ## Multiple Hooks Merging
166
+
167
+ When multiple PermissionRequest hooks are executed, their outputs merge with these rules:
168
+
169
+ - **Decision**: `deny` > `allow` (most restrictive wins)
170
+ - **Reasons**: All reasons are concatenated with `'; '`
171
+ - **Updated Input**: Last updated input wins (most recent transformation)
172
+
173
+ ```ruby
174
+ # Hook 1
175
+ hook1.deny_permission!("Reason 1")
176
+
177
+ # Hook 2
178
+ hook2.allow_permission!("Reason 2")
179
+
180
+ # Merged result
181
+ merged = ClaudeHooks::Output::PermissionRequest.merge(
182
+ hook1.output,
183
+ hook2.output
184
+ )
185
+
186
+ merged.denied? # => true (deny wins)
187
+ merged.permission_reason # => "Reason 1; Reason 2"
188
+ ```
189
+
190
+ ## Notes
191
+
192
+ - PermissionRequest runs when Claude Code shows a permission dialog
193
+ - It can automatically allow or deny on behalf of the user
194
+ - Uses the same JSON API pattern as PreToolUse hooks
195
+ - Supports modifying tool inputs before execution
196
+ - Works with all permission modes: `default`, `plan`, `acceptEdits`, `dontAsk`, `bypassPermissions`
@@ -12,6 +12,7 @@ Input helpers to access the data provided by Claude Code through `STDIN`.
12
12
  | `tool_name` | Get the name of the tool that was used |
13
13
  | `tool_input` | Get the input that was passed to the tool |
14
14
  | `tool_response` | Get the tool's response/output |
15
+ | `tool_use_id` | Get the unique identifier for this tool use (e.g., `"toolu_01ABC123..."`) |
15
16
 
16
17
  ## Hook State Helpers
17
18
  Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
@@ -11,6 +11,7 @@ Input helpers to access the data provided by Claude Code through `STDIN`.
11
11
  |--------|-------------|
12
12
  | `tool_name` | Get the name of the tool being used |
13
13
  | `tool_input` | Get the input data for the tool |
14
+ | `tool_use_id` | Get the unique identifier for this tool use (e.g., `"toolu_01ABC123..."`) |
14
15
 
15
16
  ## Hook State Helpers
16
17
  Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
@@ -22,6 +23,7 @@ Hook state methods are helpers to modify the hook's internal state (`output_data
22
23
  | `approve_tool!(reason)` | Explicitly approve tool usage |
23
24
  | `block_tool!(reason)` | Block tool usage with feedback |
24
25
  | `ask_for_permission!(reason)` | Request user permission |
26
+ | `update_tool_input!(updated_input)` | Update the tool input and automatically approve the tool (sets `permissionDecision` to `'allow'`) |
25
27
 
26
28
  ## Output Helpers
27
29
  Output helpers provide access to the hook's output data and helper methods for working with the output state.
@@ -34,8 +36,10 @@ Output helpers provide access to the hook's output data and helper methods for w
34
36
  | `output.denied?` | Check if the tool has been denied (permission_decision == 'deny') |
35
37
  | `output.blocked?` | Alias for `denied?` |
36
38
  | `output.should_ask_permission?` | Check if user permission is required (permission_decision == 'ask') |
39
+ | `output.input_updated?` | Check if tool input has been updated |
37
40
  | `output.permission_decision` | Get the permission decision: 'allow', 'deny', or 'ask' |
38
41
  | `output.permission_reason` | Get the reason for the permission decision |
42
+ | `output.updated_input` | Get the updated input (if provided) |
39
43
 
40
44
  ## Hook Exit Codes
41
45
 
@@ -9,7 +9,7 @@ Input helpers to access the data provided by Claude Code through `STDIN`.
9
9
 
10
10
  | Method | Description |
11
11
  |--------|-------------|
12
- | `source` | Get the session start source: `'startup'`, `'resume'`, or `'clear'` |
12
+ | `source` | Get the session start source: `'startup'`, `'resume'`, `'clear'`, or `'compact'` |
13
13
 
14
14
  ## Hook State Helpers
15
15
  Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
data/docs/WHY.md CHANGED
@@ -3,6 +3,7 @@
3
3
  When creating fairly complex hook systems for Claude Code it is easy to end up either with:
4
4
  - Fairly monolithic scripts that becomes hard to maintain and have to handle complex merging logic for the output.
5
5
  - A lot of scripts set up in the `settings.json` that all use the same hook input / output logic that needs to be rewritten multiple times.
6
+ - Having to understand the very chaotic exit code and output steam logic of Claude Code and how to handle it.
6
7
 
7
8
  Furthermore, Claude's documentation on hooks is a bit hard to navigate and setting up hooks is not necessarily intuitive or straightforward having to play around with STDIN, STDOUT, STDERR, exit codes, etc.
8
9
 
@@ -25,9 +26,9 @@ settings.json
25
26
 
26
27
  ## For both approaches it will bring
27
28
 
28
- 1. Normalized Input/Output handling - input parsing, validation, straightforward output helpers (block_tool!, add_context!).
29
+ 1. Normalized Input/Output handling - input parsing, validation, straightforward output helpers (`ask_for_permission!`, `approve_tool!`, `block_tool!`, `block_prompt!`, `add_context!`).
29
30
 
30
- 2. Hook-Specific APIs - 8 different hook types with tailored methods (e.g., ask_for_permission! vs block_tool!) and smart merge logic for combining outputs.
31
+ 2. Hook-Specific APIs - all 9 hook types with tailored methods and output objects (with `output_and_exit`) and smart merge logic for combining outputs.
31
32
 
32
33
  3. Session-Based Logging - Dedicated logger to understand the flow of what happens in Claude Code and write it out to a `session-{session_id}.log` file.
33
34
 
@@ -35,7 +36,6 @@ settings.json
35
36
 
36
37
  5. Testing Support - Standalone execution mode for individual hook testing and CLI testing with sample JSON input.
37
38
 
38
-
39
39
  ## For a monolithic approach it will additionally bring
40
40
 
41
41
  1. Composable Hook Handlers
@@ -43,11 +43,19 @@ For instance, `entrypoints/user_prompt_submit.rb` orchestrates multiple handlers
43
43
  ```ruby
44
44
  # Add contextual rules
45
45
  append_rules_result = AppendRules.new(input_data).call
46
+ append_rules_result.call
47
+
46
48
  # Audit logging
47
49
  log_result = LogUserPrompt.new(input_data).call
48
-
49
- # Merge outputs to Claude Code
50
- puts ClaudeHooks::UserPromptSubmit.merge_outputs(append_rules_result, log_result)
50
+ log_result.call
51
+
52
+ # Merge outputs and yield back to Claude Code
53
+ merged = ClaudeHooks::Output::UserPromptSubmit.merge(
54
+ append_rules.output,
55
+ log_handler.output
56
+ )
57
+ merged.output_and_exit
58
+ end
51
59
  ```
52
60
 
53
61
  2. Intelligent Output Merging
@@ -59,7 +67,6 @@ puts ClaudeHooks::UserPromptSubmit.merge_outputs(append_rules_result, log_result
59
67
  Each handler can run standalone for testing:
60
68
  ```ruby
61
69
  if __FILE__ == $0
62
- hook = AppendRules.new(JSON.parse(STDIN.read))
63
- puts hook.stringify_output
70
+ ClaudeHooks::CLI.test_runner(AppendRules)
64
71
  end
65
72
  ```