claude_hooks 0.2.1 → 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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -0
  3. data/README.md +215 -357
  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 +1 -1
  21. data/example_dotclaude/hooks/handlers/user_prompt_submit/log_user_prompt.rb +2 -2
  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/logger.rb +0 -1
  26. data/lib/claude_hooks/output/base.rb +152 -0
  27. data/lib/claude_hooks/output/notification.rb +22 -0
  28. data/lib/claude_hooks/output/post_tool_use.rb +68 -0
  29. data/lib/claude_hooks/output/pre_compact.rb +20 -0
  30. data/lib/claude_hooks/output/pre_tool_use.rb +97 -0
  31. data/lib/claude_hooks/output/session_end.rb +24 -0
  32. data/lib/claude_hooks/output/session_start.rb +49 -0
  33. data/lib/claude_hooks/output/stop.rb +76 -0
  34. data/lib/claude_hooks/output/subagent_stop.rb +14 -0
  35. data/lib/claude_hooks/output/user_prompt_submit.rb +71 -0
  36. data/lib/claude_hooks/post_tool_use.rb +6 -12
  37. data/lib/claude_hooks/pre_tool_use.rb +0 -37
  38. data/lib/claude_hooks/session_end.rb +43 -0
  39. data/lib/claude_hooks/session_start.rb +0 -23
  40. data/lib/claude_hooks/stop.rb +0 -25
  41. data/lib/claude_hooks/user_prompt_submit.rb +0 -26
  42. data/lib/claude_hooks/version.rb +1 -1
  43. data/lib/claude_hooks.rb +15 -0
  44. metadata +33 -8
  45. /data/{WHY.md → docs/WHY.md} +0 -0
@@ -0,0 +1,228 @@
1
+ # Migration Guide: Using Output Objects
2
+
3
+ This guide shows how to migrate from the old manual exit code patterns to the new simplified output object system.
4
+
5
+ ## Problem: The Old Way (Before)
6
+
7
+ ```ruby
8
+ #!/usr/bin/env ruby
9
+
10
+ require 'claude_hooks'
11
+ require_relative '../handlers/pre_tool_use/github_guard'
12
+
13
+ begin
14
+ # Read Claude Code input from stdin
15
+ input_data = JSON.parse(STDIN.read)
16
+
17
+ github_guard = GithubGuard.new(input_data)
18
+ guard_output = github_guard.call
19
+
20
+ # MANUAL: Extract hook-specific decision
21
+ hook_specific_decision = guard_output['hookSpecificOutput']['permissionDecision']
22
+ is_allowed_to_continue = guard_output['continue'] != false
23
+ final_output = github_guard.stringify_output
24
+
25
+ # MANUAL: Complex exit logic that you need to understand and get right
26
+ if is_allowed_to_continue
27
+ case hook_specific_decision
28
+ when 'block'
29
+ STDERR.puts final_output
30
+ exit 1
31
+ when 'ask'
32
+ STDERR.puts final_output
33
+ exit 2
34
+ else
35
+ puts final_output
36
+ exit 0
37
+ end
38
+ else
39
+ STDERR.puts final_output
40
+ exit 1
41
+ end
42
+ rescue StandardError => e
43
+ # MANUAL: Error handling with manual JSON generation and stream selection
44
+ puts JSON.generate({
45
+ continue: false,
46
+ stopReason: "PreToolUser Hook execution error: #{e.message}",
47
+ suppressOutput: false
48
+ })
49
+ exit 0 # Allow anyway to not block developers if there is an issue
50
+ end
51
+ ```
52
+
53
+ **Problems with this approach:**
54
+ - 30+ lines of repetitive boilerplate
55
+ - Need to understand Claude Code's internal exit code semantics
56
+ - Manual stream selection (STDOUT vs STDERR)
57
+ - Error-prone - easy to get exit codes wrong
58
+ - Need to manually extract hook-specific data from nested hashes
59
+
60
+ ## Solution: The New Way (After)
61
+
62
+ ```ruby
63
+ #!/usr/bin/env ruby
64
+
65
+ require 'claude_hooks'
66
+ require_relative '../handlers/pre_tool_use/github_guard'
67
+
68
+ begin
69
+ # Read Claude Code input from stdin
70
+ input_data = JSON.parse(STDIN.read)
71
+
72
+ github_guard = GithubGuard.new(input_data)
73
+ github_guard.call
74
+
75
+ # NEW: One line handles everything!
76
+ github_guard.output_and_exit
77
+
78
+ rescue StandardError => e
79
+ # NEW: Simple error handling
80
+ error_output = ClaudeHooks::Output::PreToolUse.new({
81
+ 'continue' => false,
82
+ 'stopReason' => "Hook execution error: #{e.message}",
83
+ 'suppressOutput' => false
84
+ })
85
+ error_output.output_and_exit # Automatically uses STDERR and exit 1
86
+ end
87
+ ```
88
+
89
+ **Benefits:**
90
+ - **10 lines instead of 30+** - 70% less code
91
+ - **No Claude Code knowledge needed** - just use the gem's API
92
+ - **Automatic exit codes** based on hook-specific logic
93
+ - **Automatic stream selection** (STDOUT/STDERR)
94
+ - **Type-safe access** to hook-specific fields
95
+
96
+ ## New Output Object API
97
+
98
+ ### PreToolUse Hooks
99
+
100
+ ```ruby
101
+ hook = GithubGuard.new(input_data)
102
+ hook.call
103
+ output = hook.output
104
+
105
+ # Clean semantic access
106
+ puts "Permission: #{output.permission_decision}" # 'allow', 'deny', or 'ask'
107
+ puts "Reason: #{output.permission_reason}"
108
+ puts "Allowed? #{output.allowed?}"
109
+ puts "Should ask? #{output.should_ask_permission?}"
110
+
111
+ # Automatic exit logic
112
+ puts "Exit code: #{output.exit_code}" # 0, 1, or 2 based on permission
113
+ puts "Stream: #{output.output_stream}" # :stdout or :stderr
114
+
115
+ # One call handles everything
116
+ hook.output_and_exit # Prints JSON to correct stream and exits with correct code
117
+ ```
118
+
119
+ ### UserPromptSubmit Hooks
120
+
121
+ ```ruby
122
+ hook = MyPromptHook.new(input_data)
123
+ hook.call
124
+ output = hook.output
125
+
126
+ # Clean semantic access
127
+ puts "Blocked? #{output.blocked?}"
128
+ puts "Reason: #{output.reason}"
129
+ puts "Context: #{output.additional_context}"
130
+
131
+ # Automatic execution
132
+ hook.output_and_exit # Handles all the exit logic
133
+ ```
134
+
135
+ ### Stop Hooks (Special Logic)
136
+
137
+ ```ruby
138
+ hook = MyStopHook.new(input_data)
139
+ hook.call
140
+ output = hook.output
141
+
142
+ # Note: Stop hooks have inverted logic - 'decision: block' means "continue working"
143
+ puts "Should continue? #{output.should_continue?}" # true if decision == 'block'
144
+ puts "Continue instructions: #{output.continue_instructions}"
145
+
146
+ hook.output_and_exit # exit 1 for "continue", exit 0 for "stop"
147
+ ```
148
+
149
+ ## Merging Multiple Hooks
150
+
151
+ ```ruby
152
+ # OLD WAY: Manual merging with class methods
153
+ result1 = hook1.call
154
+ result2 = hook2.call
155
+ merged_hash = ClaudeHooks::PreToolUse.merge_outputs(result1, result2)
156
+ # ... then manual exit logic
157
+
158
+ # NEW WAY: Clean object-based merging
159
+ hook1.call
160
+ hook2.call
161
+ merged_output = ClaudeHooks::Output::PreToolUse.merge(
162
+ hook1.output,
163
+ hook2.output
164
+ )
165
+ # /!\ Called on the merged output
166
+ merged_output.output_and_exit # Handles everything automatically
167
+ ```
168
+
169
+ ## Simplified Entrypoint Pattern
170
+
171
+ ### For Single Hooks
172
+ ```ruby
173
+ #!/usr/bin/env ruby
174
+ require 'claude_hooks'
175
+ require_relative '../handlers/my_hook'
176
+
177
+ # Super simple - just 3 lines!
178
+ ClaudeHooks::CLI.entrypoint do |input_data|
179
+ hook = MyHook.new(input_data)
180
+ hook.call
181
+ hook.output_and_exit
182
+ end
183
+ ```
184
+
185
+ ### For Multiple Hooks with Merging
186
+ ```ruby
187
+ #!/usr/bin/env ruby
188
+ require 'claude_hooks'
189
+ require_relative '../handlers/hook1'
190
+ require_relative '../handlers/hook2'
191
+
192
+ ClaudeHooks::CLI.entrypoint do |input_data|
193
+ hook1 = Hook1.new(input_data)
194
+ hook2 = Hook2.new(input_data)
195
+ hook1.call
196
+ hook2.call
197
+
198
+ merged = ClaudeHooks::Output::PreToolUse.merge(
199
+ hook1.output,
200
+ hook2.output
201
+ )
202
+ # /!\ Called on the merged output
203
+ merged.output_and_exit
204
+ end
205
+ ```
206
+
207
+ ## All Supported Hook Types
208
+
209
+ - `ClaudeHooks::Output::UserPromptSubmit`
210
+ - `ClaudeHooks::Output::PreToolUse`
211
+ - `ClaudeHooks::Output::PostToolUse`
212
+ - `ClaudeHooks::Output::Stop`
213
+ - `ClaudeHooks::Output::SubagentStop`
214
+ - `ClaudeHooks::Output::Notification`
215
+ - `ClaudeHooks::Output::SessionStart`
216
+ - `ClaudeHooks::Output::PreCompact`
217
+
218
+ Each handles the specific exit code logic and semantic helpers for its hook type.
219
+
220
+ ## Migration Steps
221
+
222
+ 1. **Update your hook handlers** to return `output_data` (most already do)
223
+ 2. **Replace manual exit logic** with `hook.output_and_exit` or `output.output_and_exit`
224
+ 3. **Use semantic helpers** instead of digging through hash structures
225
+ 4. **Use output object merging** instead of class methods
226
+ 5. **Enjoy the simplified, cleaner code!**
227
+
228
+ The old patterns still work for backward compatibility, but the new output objects make everything much cleaner and less error-prone.
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require_relative '../handlers/session_end/cleanup_handler'
5
+ require_relative '../handlers/session_end/log_session_stats'
6
+
7
+ begin
8
+ # Read input from stdin
9
+ input_data = JSON.parse(STDIN.read)
10
+
11
+ # Initialize handlers
12
+ cleanup_handler = CleanupHandler.new(input_data)
13
+ log_handler = LogSessionStats.new(input_data)
14
+
15
+ # Execute handlers
16
+ cleanup_handler.call
17
+ log_handler.call
18
+
19
+ # Merge outputs using the SessionEnd output merger
20
+ merged_output = ClaudeHooks::Output::SessionEnd.merge(
21
+ cleanup_handler.output,
22
+ log_handler.output
23
+ )
24
+
25
+ # Output result and exit with appropriate code
26
+ merged_output.output_and_exit
27
+
28
+ rescue StandardError => e
29
+ STDERR.puts JSON.generate({
30
+ continue: false,
31
+ stopReason: "Hook execution error: #{e.message}",
32
+ suppressOutput: false
33
+ })
34
+ exit 2
35
+ end
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ # Example of the NEW simplified entrypoint pattern using output objects
4
+ # Compare this to the existing user_prompt_submit.rb to see the difference!
5
+
3
6
  require 'claude_hooks'
4
7
  require 'json'
8
+ # Require the output classes
9
+ require_relative '../../../lib/claude_hooks/output/base'
10
+ require_relative '../../../lib/claude_hooks/output/user_prompt_submit'
5
11
  require_relative '../handlers/user_prompt_submit/append_rules'
6
12
  require_relative '../handlers/user_prompt_submit/log_user_prompt'
7
13
 
@@ -11,25 +17,24 @@ begin
11
17
 
12
18
  # Execute all hook scripts
13
19
  append_rules = AppendRules.new(input_data)
14
- appended_rules_result = append_rules.call
20
+ append_rules.call
15
21
 
16
22
  log_user_prompt = LogUserPrompt.new(input_data)
17
- log_user_prompt_result = log_user_prompt.call
23
+ log_user_prompt.call
18
24
 
19
- # Merge all hook results intelligently using the UserPromptSubmit class method
20
- hook_output = ClaudeHooks::UserPromptSubmit.merge_outputs(appended_rules_result, log_user_prompt_result)
25
+ merged_output = ClaudeHooks::Output::UserPromptSubmit.merge(
26
+ append_rules.output,
27
+ log_user_prompt.output
28
+ )
21
29
 
22
- # Output final merged result to Claude Code
23
- puts JSON.generate(hook_output)
30
+ merged_output.output_and_exit
24
31
 
25
- exit 0 # Don't forget to exit with the right exit code (0, 1, 2)
26
32
  rescue StandardError => e
27
- STDERR.puts "Error in UserPromptSubmit hook: #{e.message} #{e.backtrace.join("\n")}"
28
-
29
- puts JSON.generate({
33
+ # Same simple error pattern
34
+ STDERR.puts JSON.generate({
30
35
  continue: false,
31
36
  stopReason: "Hook execution error: #{e.message} #{e.backtrace.join("\n")}",
32
37
  suppressOutput: false
33
38
  })
34
- exit 1
35
- end
39
+ exit 2
40
+ end
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../../../../lib/claude_hooks'
4
+
5
+ class CleanupHandler < ClaudeHooks::SessionEnd
6
+ def call
7
+ log "Session ended with reason: #{reason}"
8
+
9
+ case reason
10
+ when 'clear'
11
+ log "Performing cleanup after /clear command"
12
+ cleanup_temp_files
13
+ when 'logout'
14
+ log "Performing cleanup after logout"
15
+ save_session_state
16
+ when 'prompt_input_exit'
17
+ log "User exited during prompt input"
18
+ save_partial_state
19
+ else
20
+ log "General session cleanup"
21
+ general_cleanup
22
+ end
23
+
24
+ output
25
+ end
26
+
27
+ private
28
+
29
+ def cleanup_temp_files
30
+ log "Cleaning up temporary files for session #{session_id}"
31
+ # Example cleanup logic
32
+ end
33
+
34
+ def save_session_state
35
+ log "Saving session state for session #{session_id}"
36
+ # Example state saving logic
37
+ end
38
+
39
+ def save_partial_state
40
+ log "Saving partial state for session #{session_id}"
41
+ # Example partial state saving logic
42
+ end
43
+
44
+ def general_cleanup
45
+ log "Performing general cleanup for session #{session_id}"
46
+ # Example general cleanup logic
47
+ end
48
+ end
49
+
50
+ # CLI testing support
51
+ if __FILE__ == $0
52
+ ClaudeHooks::CLI.test_runner(CleanupHandler) do |input_data|
53
+ input_data['reason'] ||= 'other'
54
+ end
55
+ end
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'claude_hooks'
4
+
5
+ class LogSessionStats < ClaudeHooks::SessionEnd
6
+ def call
7
+ log "Logging session statistics for session #{session_id}"
8
+
9
+ # Generate session statistics
10
+ stats = gather_session_stats
11
+
12
+ # Log detailed statistics
13
+ log <<~STATS
14
+ === Session Statistics ===
15
+ Session ID: #{session_id}
16
+ End Reason: #{reason}
17
+ Duration: #{stats[:duration]}
18
+ Working Directory: #{cwd}
19
+ Transcript Path: #{transcript_path}
20
+ ===========================
21
+ STATS
22
+
23
+ # Log session summary
24
+ log format_session_summary(stats)
25
+
26
+ output
27
+ end
28
+
29
+ private
30
+
31
+ def gather_session_stats
32
+ {
33
+ duration: calculate_session_duration,
34
+ end_time: Time.now,
35
+ transcript_size: get_transcript_size
36
+ }
37
+ end
38
+
39
+ def calculate_session_duration
40
+ # Example: could read session start time from transcript or file
41
+ "Unknown (would need session start time)"
42
+ end
43
+
44
+ def get_transcript_size
45
+ return 0 unless transcript_path && File.exist?(transcript_path)
46
+
47
+ File.size(transcript_path)
48
+ rescue StandardError => e
49
+ log "Error getting transcript size: #{e.message}", level: :warn
50
+ 0
51
+ end
52
+
53
+ def format_session_summary(stats)
54
+ "Session #{session_id} ended (#{reason}): #{stats[:transcript_size]} bytes transcript"
55
+ end
56
+ end
57
+
58
+ # CLI testing support
59
+ if __FILE__ == $0
60
+ ClaudeHooks::CLI.test_runner(LogSessionStats) do |input_data|
61
+ input_data['reason'] ||= 'other'
62
+ input_data['transcript_path'] ||= '/tmp/test_transcript'
63
+ end
64
+ end
@@ -18,7 +18,7 @@ class AppendRules < ClaudeHooks::UserPromptSubmit
18
18
  log "No rule content found", level: :warn
19
19
  end
20
20
 
21
- output_data
21
+ output
22
22
  end
23
23
 
24
24
  private
@@ -12,10 +12,10 @@ class LogUserPrompt < ClaudeHooks::UserPromptSubmit
12
12
 
13
13
  log <<~TEXT
14
14
  Prompt: #{current_prompt}
15
- Logged user prompt to #{log_file_path}
15
+ Logged user prompt (session: #{session_id})
16
16
  TEXT
17
17
 
18
- nil
18
+ nil # ignored output
19
19
  end
20
20
  end
21
21
 
@@ -14,6 +14,17 @@
14
14
  }
15
15
  ]
16
16
  }
17
+ ],
18
+ "SessionEnd": [
19
+ {
20
+ "matcher": "",
21
+ "hooks": [
22
+ {
23
+ "type": "command",
24
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/entrypoints/session_end.rb"
25
+ }
26
+ ]
27
+ }
17
28
  ]
18
29
  },
19
30
  "env": {
@@ -3,6 +3,7 @@
3
3
  require 'json'
4
4
  require_relative 'configuration'
5
5
  require_relative 'logger'
6
+ require_relative 'output/base'
6
7
 
7
8
  module ClaudeHooks
8
9
  # Base class for Claude Code hook scripts
@@ -24,7 +25,7 @@ module ClaudeHooks
24
25
  self.class.hook_type
25
26
  end
26
27
 
27
- attr_reader :config, :input_data, :output_data, :logger
28
+ attr_reader :config, :input_data, :output_data, :output, :logger
28
29
  def initialize(input_data = {})
29
30
  @config = Configuration
30
31
  @input_data = input_data
@@ -33,6 +34,7 @@ module ClaudeHooks
33
34
  'stopReason' => '',
34
35
  'suppressOutput' => false
35
36
  }
37
+ @output = ClaudeHooks::Output::Base.for_hook_type(hook_type, @output_data)
36
38
  @logger = Logger.new(session_id, self.class.name)
37
39
 
38
40
  validate_input!
@@ -47,6 +49,10 @@ module ClaudeHooks
47
49
  JSON.generate(@output_data)
48
50
  end
49
51
 
52
+ def output_and_exit
53
+ @output.output_and_exit
54
+ end
55
+
50
56
  # === COMMON INPUT DATA ACCESS ===
51
57
 
52
58
  def session_id
@@ -105,6 +111,15 @@ module ClaudeHooks
105
111
  @output_data['hookSpecificOutput'] = nil
106
112
  end
107
113
 
114
+ # System message shown to the user (not to Claude)
115
+ def system_message!(message)
116
+ @output_data['systemMessage'] = message
117
+ end
118
+
119
+ def clear_system_message!
120
+ @output_data.delete('systemMessage')
121
+ end
122
+
108
123
  # === CONFIG AND UTILITY METHODS ===
109
124
 
110
125
  def base_dir
@@ -136,29 +151,6 @@ module ClaudeHooks
136
151
  @logger.log(message, level: level, &block)
137
152
  end
138
153
 
139
- protected
140
-
141
- # === MERGE HELPER BASE ===
142
-
143
- # Handles common merging logic for all hook types
144
- # Subclasses should call super and add their specific logic
145
- def self.merge_outputs(*outputs_data)
146
- compacted_outputs_data = outputs_data.compact
147
- return { 'continue' => true, 'stopReason' => '', 'suppressOutput' => false } if compacted_outputs_data.empty?
148
-
149
- # Initialize merged result with defaults
150
- merged = { 'continue' => true, 'stopReason' => '', 'suppressOutput' => false }
151
-
152
- # Apply common merge logic
153
- compacted_outputs_data.each do |output|
154
- merged['continue'] = false if output['continue'] == false
155
- merged['stopReason'] = [merged['stopReason'], output['stopReason']].compact.reject(&:empty?).join('; ')
156
- merged['suppressOutput'] = true if output['suppressOutput'] == true
157
- end
158
-
159
- merged
160
- end
161
-
162
154
  private
163
155
 
164
156
  def validate_input!
@@ -89,6 +89,78 @@ module ClaudeHooks
89
89
  run_hook(hook_class, merged_data)
90
90
  end
91
91
 
92
+ # Simplified entrypoint helper for hook scripts
93
+ # This handles all the STDIN reading, JSON parsing, error handling, and output execution
94
+ #
95
+ # Usage patterns:
96
+ #
97
+ # 1. Block form - custom logic:
98
+ # ClaudeHooks::CLI.entrypoint do |input_data|
99
+ # hook = MyHook.new(input_data)
100
+ # hook.call
101
+ # hook.output_and_exit
102
+ # end
103
+ #
104
+ # 2. Simple form - single hook class:
105
+ # ClaudeHooks::CLI.entrypoint(MyHook)
106
+ #
107
+ # 3. Multiple hooks with merging:
108
+ # ClaudeHooks::CLI.entrypoint do |input_data|
109
+ # hook1 = Hook1.new(input_data)
110
+ # hook2 = Hook2.new(input_data)
111
+ # result1 = hook1.call
112
+ # result2 = hook2.call
113
+ #
114
+ # # Use the appropriate output class for merging
115
+ # merged = ClaudeHooks::Output::PreToolUse.merge(
116
+ # hook1.output,
117
+ # hook2.output
118
+ # )
119
+ # merged.output_and_exit
120
+ # end
121
+ def entrypoint(hook_class = nil, &block)
122
+ # Read and parse input from STDIN
123
+ input_data = JSON.parse(STDIN.read)
124
+
125
+ if block_given?
126
+ # Custom block form
127
+ yield(input_data)
128
+ elsif hook_class
129
+ # Simple single hook form
130
+ hook = hook_class.new(input_data)
131
+ hook.call
132
+ hook.output_and_exit
133
+ else
134
+ raise ArgumentError, "Either provide a hook_class or a block"
135
+ end
136
+
137
+ rescue JSON::ParserError => e
138
+ STDERR.puts "JSON parsing error: #{e.message}"
139
+ error_response = {
140
+ continue: false,
141
+ stopReason: "JSON parsing error: #{e.message}",
142
+ suppressOutput: false
143
+ }
144
+ response = JSON.generate(error_response)
145
+ puts response
146
+ STDERR.puts response
147
+ exit 1
148
+
149
+ rescue StandardError => e
150
+ STDERR.puts "Hook execution error: #{e.message}"
151
+ STDERR.puts e.backtrace.join("\n") if e.backtrace
152
+
153
+ error_response = {
154
+ continue: false,
155
+ stopReason: "Hook execution error: #{e.message}",
156
+ suppressOutput: false
157
+ }
158
+ response = JSON.generate(error_response)
159
+ puts response
160
+ STDERR.puts response
161
+ exit 1
162
+ end
163
+
92
164
  private
93
165
 
94
166
  def read_stdin_input
@@ -111,7 +183,9 @@ module ClaudeHooks
111
183
  suppressOutput: false
112
184
  }
113
185
 
114
- puts JSON.generate(error_response)
186
+ response = JSON.generate(error_response)
187
+ puts response
188
+ STDERR.puts response
115
189
  exit 1
116
190
  end
117
191
  end
@@ -5,7 +5,6 @@ require_relative 'configuration'
5
5
 
6
6
  module ClaudeHooks
7
7
  # Session-based logger for Claude Code hooks
8
- # Provides both single-line and multiline block-based logging
9
8
  class Logger
10
9
  def initialize(session_id, source)
11
10
  @session_id = session_id