claude_hooks 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/skills/ci-monitoring/SKILL.md +103 -0
  3. data/.agents/skills/run-tests/SKILL.md +45 -0
  4. data/AGENTS.md +46 -0
  5. data/CHANGELOG.md +64 -0
  6. data/README.md +303 -239
  7. data/docs/1.0.0_MIGRATION_GUIDE.md +6 -7
  8. data/docs/API/COMMON.md +7 -1
  9. data/docs/API/CONFIG_CHANGE.md +39 -0
  10. data/docs/API/CWD_CHANGED.md +34 -0
  11. data/docs/API/ELICITATION.md +44 -0
  12. data/docs/API/ELICITATION_RESULT.md +43 -0
  13. data/docs/API/FILE_CHANGED.md +37 -0
  14. data/docs/API/INSTRUCTIONS_LOADED.md +30 -0
  15. data/docs/API/MESSAGE_DISPLAY.md +38 -0
  16. data/docs/API/PERMISSION_DENIED.md +37 -0
  17. data/docs/API/PERMISSION_REQUEST.md +11 -8
  18. data/docs/API/POST_COMPACT.md +30 -0
  19. data/docs/API/POST_TOOL_BATCH.md +39 -0
  20. data/docs/API/POST_TOOL_USE_FAILURE.md +31 -0
  21. data/docs/API/PRE_COMPACT.md +10 -2
  22. data/docs/API/PRE_TOOL_USE.md +5 -1
  23. data/docs/API/SETUP.md +37 -0
  24. data/docs/API/STOP_FAILURE.md +19 -0
  25. data/docs/API/SUBAGENT_START.md +40 -0
  26. data/docs/API/TASK_COMPLETED.md +43 -0
  27. data/docs/API/TASK_CREATED.md +43 -0
  28. data/docs/API/TEAMMATE_IDLE.md +40 -0
  29. data/docs/API/USER_PROMPT_EXPANSION.md +43 -0
  30. data/docs/API/WORKTREE_CREATE.md +39 -0
  31. data/docs/API/WORKTREE_REMOVE.md +33 -0
  32. data/docs/PROMPT_BASED_HOOKS.md +386 -0
  33. data/docs/external/claude-hooks-reference.md +2406 -854
  34. data/docs/mitts/ideabox.md +12 -0
  35. data/docs/mitts/prd.md +417 -0
  36. data/docs/mitts/setup.md +303 -0
  37. data/docs/mitts/task.md +44 -0
  38. data/example_dotclaude/hooks/entrypoints/session_end.rb +5 -22
  39. data/example_dotclaude/hooks/entrypoints/user_prompt_submit.rb +4 -26
  40. data/example_dotclaude/hooks/{handlers/pre_tool_use/github_guard.rb → github_guard.rb} +7 -13
  41. data/example_dotclaude/settings.json +1 -1
  42. data/lib/claude_hooks/base.rb +25 -0
  43. data/lib/claude_hooks/cli.rb +84 -149
  44. data/lib/claude_hooks/config_change.rb +28 -0
  45. data/lib/claude_hooks/cwd_changed.rb +28 -0
  46. data/lib/claude_hooks/elicitation.rb +61 -0
  47. data/lib/claude_hooks/elicitation_result.rb +57 -0
  48. data/lib/claude_hooks/file_changed.rb +41 -0
  49. data/lib/claude_hooks/instructions_loaded.rb +23 -0
  50. data/lib/claude_hooks/message_display.rb +58 -0
  51. data/lib/claude_hooks/output/base.rb +46 -1
  52. data/lib/claude_hooks/output/config_change.rb +47 -0
  53. data/lib/claude_hooks/output/cwd_changed.rb +42 -0
  54. data/lib/claude_hooks/output/elicitation.rb +67 -0
  55. data/lib/claude_hooks/output/elicitation_result.rb +63 -0
  56. data/lib/claude_hooks/output/file_changed.rb +42 -0
  57. data/lib/claude_hooks/output/instructions_loaded.rb +19 -0
  58. data/lib/claude_hooks/output/message_display.rb +49 -0
  59. data/lib/claude_hooks/output/permission_denied.rb +42 -0
  60. data/lib/claude_hooks/output/permission_request.rb +50 -25
  61. data/lib/claude_hooks/output/post_compact.rb +18 -0
  62. data/lib/claude_hooks/output/post_tool_batch.rb +51 -0
  63. data/lib/claude_hooks/output/post_tool_use.rb +12 -0
  64. data/lib/claude_hooks/output/post_tool_use_failure.rb +42 -0
  65. data/lib/claude_hooks/output/pre_compact.rb +34 -3
  66. data/lib/claude_hooks/output/pre_tool_use.rb +8 -2
  67. data/lib/claude_hooks/output/session_start.rb +30 -8
  68. data/lib/claude_hooks/output/setup.rb +42 -0
  69. data/lib/claude_hooks/output/stop.rb +15 -0
  70. data/lib/claude_hooks/output/stop_failure.rb +19 -0
  71. data/lib/claude_hooks/output/subagent_start.rb +42 -0
  72. data/lib/claude_hooks/output/task_completed.rb +18 -0
  73. data/lib/claude_hooks/output/task_created.rb +18 -0
  74. data/lib/claude_hooks/output/teammate_idle.rb +18 -0
  75. data/lib/claude_hooks/output/user_prompt_expansion.rb +51 -0
  76. data/lib/claude_hooks/output/worktree_create.rb +60 -0
  77. data/lib/claude_hooks/output/worktree_remove.rb +19 -0
  78. data/lib/claude_hooks/permission_denied.rb +41 -0
  79. data/lib/claude_hooks/permission_request.rb +16 -9
  80. data/lib/claude_hooks/post_compact.rb +23 -0
  81. data/lib/claude_hooks/post_tool_batch.rb +59 -0
  82. data/lib/claude_hooks/post_tool_use.rb +10 -0
  83. data/lib/claude_hooks/post_tool_use_failure.rb +45 -0
  84. data/lib/claude_hooks/pre_compact.rb +7 -0
  85. data/lib/claude_hooks/pre_tool_use.rb +20 -2
  86. data/lib/claude_hooks/session_start.rb +28 -0
  87. data/lib/claude_hooks/setup.rb +25 -0
  88. data/lib/claude_hooks/stop.rb +17 -0
  89. data/lib/claude_hooks/stop_failure.rb +27 -0
  90. data/lib/claude_hooks/subagent_start.rb +23 -0
  91. data/lib/claude_hooks/subagent_stop.rb +5 -1
  92. data/lib/claude_hooks/task_completed.rb +35 -0
  93. data/lib/claude_hooks/task_created.rb +35 -0
  94. data/lib/claude_hooks/teammate_idle.rb +23 -0
  95. data/lib/claude_hooks/user_prompt_expansion.rb +45 -0
  96. data/lib/claude_hooks/version.rb +1 -1
  97. data/lib/claude_hooks/worktree_create.rb +27 -0
  98. data/lib/claude_hooks/worktree_remove.rb +19 -0
  99. data/lib/claude_hooks.rb +40 -0
  100. metadata +71 -4
  101. data/example_dotclaude/hooks/entrypoints/pre_tool_use.rb +0 -25
@@ -3,191 +3,126 @@
3
3
  require 'json'
4
4
 
5
5
  module ClaudeHooks
6
- # CLI utility for testing hook handlers in isolation
7
- # This module provides a standardized way to run hooks directly from the command line
8
- # for testing and debugging purposes.
9
6
  module CLI
10
7
  class << self
11
- # Run a hook class directly from command line
12
- # Usage:
13
- # ClaudeHooks::CLI.run_hook(YourHookClass)
14
- # ClaudeHooks::CLI.run_hook(YourHookClass, custom_input_data)
15
- #
16
- # # With customization block:
17
- # ClaudeHooks::CLI.run_hook(YourHookClass) do |input_data|
18
- # input_data['debug_mode'] = true
19
- # end
20
- def run_hook(hook_class, input_data = nil, &block)
21
- # If no input data provided, read from STDIN
22
- input_data ||= read_stdin_input
23
-
24
- # Apply customization block if provided
8
+ # Run a hook script from the command line.
9
+ # Reads JSON from STDIN, calls hook.call, and exits with the correct code.
10
+ #
11
+ # on_error controls what happens when the hook raises an unexpected exception:
12
+ # :allow (default) — exit 1, non-blocking; Claude continues as if the hook didn't run.
13
+ # :block — exit 2, blocking; Claude stops and shows the error. Use this for
14
+ # security/policy hooks where a crash should never silently pass through.
15
+ #
16
+ # Usage patterns:
17
+ #
18
+ # 1. Single hook class:
19
+ # ClaudeHooks::CLI.run_hook(MyHook)
20
+ # ClaudeHooks::CLI.run_hook(MyHook, on_error: :block) # fail-closed
21
+ #
22
+ # 2. Multiple hooks with merging:
23
+ # ClaudeHooks::CLI.run_hook(on_error: :block) do |input_data|
24
+ # hook1 = Hook1.new(input_data)
25
+ # hook2 = Hook2.new(input_data)
26
+ # hook1.call
27
+ # hook2.call
28
+ # ClaudeHooks::Output::PreToolUse.merge(hook1.output, hook2.output).output_and_exit
29
+ # end
30
+ def run_hook(hook_class = nil, on_error: :allow, &block)
31
+ input_data = JSON.parse(STDIN.read)
32
+
25
33
  if block_given?
26
34
  yield(input_data)
35
+ elsif hook_class
36
+ hook = hook_class.new(input_data)
37
+ hook.call
38
+ hook.output_and_exit
39
+ else
40
+ raise ArgumentError, "Either provide a hook_class or a block"
27
41
  end
28
-
29
- # Create and execute the hook
30
- hook = hook_class.new(input_data)
31
- result = hook.call
32
-
33
- # Output the result as JSON (same format as production hooks)
34
- puts JSON.generate(result) if result
35
-
36
- result
42
+
43
+ rescue JSON::ParserError => e
44
+ handle_run_error("JSON parsing error: #{e.message}", on_error)
45
+
37
46
  rescue StandardError => e
38
- handle_error(e, hook_class)
47
+ handle_run_error("Hook execution error: #{e.message}", on_error, backtrace: e.backtrace)
39
48
  end
40
49
 
41
- # Create a test runner block for a hook class
42
- # This generates the common if __FILE__ == $0 block content
43
- #
44
- # Usage:
45
- # ClaudeHooks::CLI.test_runner(YourHookClass)
46
- #
47
- # # With customization block:
48
- # ClaudeHooks::CLI.test_runner(YourHookClass) do |input_data|
49
- # input_data['custom_field'] = 'test_value'
50
- # input_data['user_name'] = 'TestUser'
51
- # end
50
+ # @deprecated Use {run_hook} instead.
51
+ def entrypoint(hook_class = nil, on_error: :allow, &block)
52
+ warn "[ClaudeHooks] CLI.entrypoint is deprecated — use CLI.run_hook instead."
53
+ run_hook(hook_class, on_error: on_error, &block)
54
+ end
55
+
56
+ # Testing helpers — use these inside `if __FILE__ == $0` blocks, not in production.
57
+
58
+ # Run a hook with input read from STDIN, with optional block to mutate input_data before running.
52
59
  def test_runner(hook_class, &block)
53
60
  input_data = read_stdin_input
54
-
55
- # Apply customization block if provided
56
- if block_given?
57
- yield(input_data)
58
- end
59
-
60
- run_hook(hook_class, input_data)
61
+ yield(input_data) if block_given?
62
+ run_hook_with_data(hook_class, input_data)
61
63
  end
62
64
 
63
- # Run hook with sample data (useful for development)
64
- # Usage:
65
- # ClaudeHooks::CLI.run_with_sample_data(YourHookClass)
66
- # ClaudeHooks::CLI.run_with_sample_data(YourHookClass, { 'prompt' => 'test prompt' })
67
- #
68
- # # With customization block:
69
- # ClaudeHooks::CLI.run_with_sample_data(YourHookClass) do |input_data|
70
- # input_data['prompt'] = 'Custom test prompt'
71
- # input_data['debug'] = true
72
- # end
65
+ # Run a hook with synthetic sample data (no STDIN needed).
73
66
  def run_with_sample_data(hook_class, sample_data = {}, &block)
74
- default_sample = {
67
+ input_data = {
75
68
  'session_id' => 'test-session',
76
69
  'transcript_path' => '/tmp/test_transcript.md',
77
70
  'cwd' => Dir.pwd,
78
71
  'hook_event_name' => hook_class.hook_type
79
- }
72
+ }.merge(sample_data)
80
73
 
81
- # Merge with hook-specific sample data
82
- merged_data = default_sample.merge(sample_data)
83
-
84
- # Apply customization block if provided
85
- if block_given?
86
- yield(merged_data)
87
- end
88
-
89
- run_hook(hook_class, merged_data)
74
+ yield(input_data) if block_given?
75
+ run_hook_with_data(hook_class, input_data)
90
76
  end
91
77
 
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
-
78
+ private
79
+
80
+ # Runs a hook with already-parsed input_data. Returns the result without exiting.
81
+ # Used internally by test_runner and run_with_sample_data.
82
+ def run_hook_with_data(hook_class, input_data)
83
+ hook = hook_class.new(input_data)
84
+ result = hook.call
85
+ puts JSON.generate(result) if result
86
+ result
149
87
  rescue StandardError => e
150
- STDERR.puts "Hook execution error: #{e.message}"
88
+ hook_name = hook_class.name || hook_class.to_s
89
+ STDERR.puts "Error in #{hook_name} hook: #{e.message}"
151
90
  STDERR.puts e.backtrace.join("\n") if e.backtrace
152
-
153
- error_response = {
91
+ response = JSON.generate({
154
92
  continue: false,
155
- stopReason: "Hook execution error: #{e.message}",
93
+ stopReason: "#{hook_name} execution error: #{e.message}",
156
94
  suppressOutput: false
157
- }
158
- response = JSON.generate(error_response)
95
+ })
159
96
  puts response
160
97
  STDERR.puts response
161
98
  exit 1
162
99
  end
163
100
 
164
- private
101
+ def handle_run_error(message, on_error, backtrace: nil)
102
+ if on_error == :block
103
+ # Exit 2: Claude Code shows stderr to the model as plain text (never
104
+ # parsed as JSON), so emit just the message and block.
105
+ STDERR.puts message
106
+ exit 2
107
+ else
108
+ # Exit 1: non-blocking. stderr's first line surfaces in the transcript.
109
+ STDERR.puts backtrace.join("\n") if backtrace
110
+ STDERR.puts JSON.generate({
111
+ continue: false,
112
+ stopReason: message,
113
+ suppressOutput: false
114
+ })
115
+ exit 1
116
+ end
117
+ end
165
118
 
166
119
  def read_stdin_input
167
120
  stdin_content = STDIN.read.strip
168
121
  return {} if stdin_content.empty?
169
-
170
122
  JSON.parse(stdin_content)
171
123
  rescue JSON::ParserError => e
172
124
  raise "Invalid JSON input: #{e.message}"
173
125
  end
174
-
175
- def handle_error(error, hook_class)
176
- STDERR.puts "Error in #{hook_class.name} hook: #{error.message}"
177
- STDERR.puts error.backtrace.join("\n") if error.backtrace
178
-
179
- # Output error response in Claude Code format
180
- error_response = {
181
- continue: false,
182
- stopReason: "#{hook_class.name} execution error: #{error.message}",
183
- suppressOutput: false
184
- }
185
-
186
- response = JSON.generate(error_response)
187
- puts response
188
- STDERR.puts response
189
- exit 1
190
- end
191
126
  end
192
127
  end
193
- end
128
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module ClaudeHooks
6
+ class ConfigChange < Base
7
+ def self.hook_type
8
+ 'ConfigChange'
9
+ end
10
+
11
+ def self.input_fields
12
+ %w[source]
13
+ end
14
+
15
+ def source
16
+ @input_data['source']
17
+ end
18
+
19
+ def file_path
20
+ @input_data['file_path'] || @input_data['filePath']
21
+ end
22
+
23
+ def block!(reason = '')
24
+ @output_data['decision'] = 'block'
25
+ @output_data['reason'] = reason
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module ClaudeHooks
6
+ class CwdChanged < Base
7
+ def self.hook_type
8
+ 'CwdChanged'
9
+ end
10
+
11
+ def self.input_fields
12
+ %w[old_cwd new_cwd]
13
+ end
14
+
15
+ def old_cwd
16
+ @input_data['old_cwd'] || @input_data['oldCwd']
17
+ end
18
+
19
+ def new_cwd
20
+ @input_data['new_cwd'] || @input_data['newCwd']
21
+ end
22
+
23
+ def watch_paths!(paths)
24
+ @output_data['hookSpecificOutput'] ||= { 'hookEventName' => hook_event_name }
25
+ @output_data['hookSpecificOutput']['watchPaths'] = Array(paths)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module ClaudeHooks
6
+ class Elicitation < Base
7
+ def self.hook_type
8
+ 'Elicitation'
9
+ end
10
+
11
+ def self.input_fields
12
+ %w[mcp_server_name message]
13
+ end
14
+
15
+ def mcp_server_name
16
+ @input_data['mcp_server_name'] || @input_data['mcpServerName']
17
+ end
18
+
19
+ def message
20
+ @input_data['message']
21
+ end
22
+
23
+ def mode
24
+ @input_data['mode']
25
+ end
26
+
27
+ def url
28
+ @input_data['url']
29
+ end
30
+
31
+ def elicitation_id
32
+ @input_data['elicitation_id'] || @input_data['elicitationId']
33
+ end
34
+
35
+ def requested_schema
36
+ @input_data['requested_schema'] || @input_data['requestedSchema']
37
+ end
38
+
39
+ def accept!(content = {})
40
+ @output_data['hookSpecificOutput'] = {
41
+ 'hookEventName' => hook_event_name,
42
+ 'action' => 'accept',
43
+ 'content' => content
44
+ }
45
+ end
46
+
47
+ def decline!
48
+ @output_data['hookSpecificOutput'] = {
49
+ 'hookEventName' => hook_event_name,
50
+ 'action' => 'decline'
51
+ }
52
+ end
53
+
54
+ def cancel!
55
+ @output_data['hookSpecificOutput'] = {
56
+ 'hookEventName' => hook_event_name,
57
+ 'action' => 'cancel'
58
+ }
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module ClaudeHooks
6
+ class ElicitationResult < Base
7
+ def self.hook_type
8
+ 'ElicitationResult'
9
+ end
10
+
11
+ def self.input_fields
12
+ %w[mcp_server_name action]
13
+ end
14
+
15
+ def mcp_server_name
16
+ @input_data['mcp_server_name'] || @input_data['mcpServerName']
17
+ end
18
+
19
+ def action
20
+ @input_data['action']
21
+ end
22
+
23
+ def mode
24
+ @input_data['mode']
25
+ end
26
+
27
+ def elicitation_id
28
+ @input_data['elicitation_id'] || @input_data['elicitationId']
29
+ end
30
+
31
+ def content
32
+ @input_data['content']
33
+ end
34
+
35
+ def accept!(content = {})
36
+ @output_data['hookSpecificOutput'] = {
37
+ 'hookEventName' => hook_event_name,
38
+ 'action' => 'accept',
39
+ 'content' => content
40
+ }
41
+ end
42
+
43
+ def decline!
44
+ @output_data['hookSpecificOutput'] = {
45
+ 'hookEventName' => hook_event_name,
46
+ 'action' => 'decline'
47
+ }
48
+ end
49
+
50
+ def cancel!
51
+ @output_data['hookSpecificOutput'] = {
52
+ 'hookEventName' => hook_event_name,
53
+ 'action' => 'cancel'
54
+ }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module ClaudeHooks
6
+ class FileChanged < Base
7
+ def self.hook_type
8
+ 'FileChanged'
9
+ end
10
+
11
+ def self.input_fields
12
+ %w[file_path event]
13
+ end
14
+
15
+ def file_path
16
+ @input_data['file_path'] || @input_data['filePath']
17
+ end
18
+
19
+ # Values: change | add | unlink
20
+ def event
21
+ @input_data['event']
22
+ end
23
+
24
+ def created?
25
+ event == 'add'
26
+ end
27
+
28
+ def modified?
29
+ event == 'change'
30
+ end
31
+
32
+ def deleted?
33
+ event == 'unlink'
34
+ end
35
+
36
+ def watch_paths!(paths)
37
+ @output_data['hookSpecificOutput'] ||= { 'hookEventName' => hook_event_name }
38
+ @output_data['hookSpecificOutput']['watchPaths'] = Array(paths)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module ClaudeHooks
6
+ class InstructionsLoaded < Base
7
+ def self.hook_type
8
+ 'InstructionsLoaded'
9
+ end
10
+
11
+ def self.input_fields
12
+ %w[file_path load_reason]
13
+ end
14
+
15
+ def file_path
16
+ @input_data['file_path'] || @input_data['filePath']
17
+ end
18
+
19
+ def load_reason
20
+ @input_data['load_reason'] || @input_data['loadReason']
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module ClaudeHooks
6
+ # MessageDisplay runs while assistant message text is displayed on screen.
7
+ # Display-only: exit code and decision fields are ignored. The only effect is
8
+ # replacing the on-screen text via hookSpecificOutput.displayContent — the
9
+ # transcript and what Claude sees keep the original text.
10
+ class MessageDisplay < Base
11
+ def self.hook_type
12
+ 'MessageDisplay'
13
+ end
14
+
15
+ def self.input_fields
16
+ %w[turn_id message_id index final delta]
17
+ end
18
+
19
+ # === INPUT DATA ACCESS ===
20
+
21
+ def turn_id
22
+ @input_data['turn_id'] || @input_data['turnId']
23
+ end
24
+
25
+ def message_id
26
+ @input_data['message_id'] || @input_data['messageId']
27
+ end
28
+
29
+ def index
30
+ @input_data['index']
31
+ end
32
+
33
+ def final
34
+ @input_data.key?('final') ? @input_data['final'] : @input_data['isFinal']
35
+ end
36
+ alias_method :final?, :final
37
+
38
+ # The streamed assistant text delta for this event.
39
+ def delta
40
+ @input_data['delta']
41
+ end
42
+
43
+ # Some payloads expose the full text so far; kept as a convenience reader.
44
+ def message_text
45
+ @input_data['message_text'] || @input_data['messageText']
46
+ end
47
+
48
+ # === OUTPUT DATA HELPERS ===
49
+
50
+ # Replace the text shown on screen (display-only).
51
+ def display_content!(content)
52
+ @output_data['hookSpecificOutput'] = {
53
+ 'hookEventName' => hook_event_name,
54
+ 'displayContent' => content
55
+ }
56
+ end
57
+ end
58
+ end
@@ -40,6 +40,10 @@ module ClaudeHooks
40
40
  @data['hookSpecificOutput'] || {}
41
41
  end
42
42
 
43
+ def terminal_sequence
44
+ @data['terminalSequence']
45
+ end
46
+
43
47
  # === JSON SERIALIZATION ===
44
48
 
45
49
  # Convert to JSON string (same as existing stringify_output)
@@ -100,11 +104,12 @@ module ClaudeHooks
100
104
  # Apply base merge logic
101
105
  compacted_outputs.each do |output|
102
106
  output_data = output.respond_to?(:data) ? output.data : output
103
-
107
+
104
108
  merged_data['continue'] = false if output_data['continue'] == false
105
109
  merged_data['suppressOutput'] = true if output_data['suppressOutput'] == true
106
110
  merged_data['stopReason'] = [merged_data['stopReason'], output_data['stopReason']].compact.reject(&:empty?).join('; ')
107
111
  merged_data['systemMessage'] = [merged_data['systemMessage'], output_data['systemMessage']].compact.reject(&:empty?).join('; ')
112
+ merged_data['terminalSequence'] = output_data['terminalSequence'] unless output_data['terminalSequence'].nil?
108
113
  end
109
114
 
110
115
  self.new(merged_data)
@@ -129,12 +134,52 @@ module ClaudeHooks
129
134
  SubagentStop.new(data)
130
135
  when 'Notification'
131
136
  Notification.new(data)
137
+ when 'MessageDisplay'
138
+ MessageDisplay.new(data)
132
139
  when 'SessionStart'
133
140
  SessionStart.new(data)
134
141
  when 'SessionEnd'
135
142
  SessionEnd.new(data)
136
143
  when 'PreCompact'
137
144
  PreCompact.new(data)
145
+ when 'Setup'
146
+ Setup.new(data)
147
+ when 'SubagentStart'
148
+ SubagentStart.new(data)
149
+ when 'UserPromptExpansion'
150
+ UserPromptExpansion.new(data)
151
+ when 'PostToolBatch'
152
+ PostToolBatch.new(data)
153
+ when 'ConfigChange'
154
+ ConfigChange.new(data)
155
+ when 'TaskCreated'
156
+ TaskCreated.new(data)
157
+ when 'TaskCompleted'
158
+ TaskCompleted.new(data)
159
+ when 'TeammateIdle'
160
+ TeammateIdle.new(data)
161
+ when 'PostToolUseFailure'
162
+ PostToolUseFailure.new(data)
163
+ when 'StopFailure'
164
+ StopFailure.new(data)
165
+ when 'PostCompact'
166
+ PostCompact.new(data)
167
+ when 'CwdChanged'
168
+ CwdChanged.new(data)
169
+ when 'FileChanged'
170
+ FileChanged.new(data)
171
+ when 'InstructionsLoaded'
172
+ InstructionsLoaded.new(data)
173
+ when 'WorktreeRemove'
174
+ WorktreeRemove.new(data)
175
+ when 'PermissionDenied'
176
+ PermissionDenied.new(data)
177
+ when 'Elicitation'
178
+ Elicitation.new(data)
179
+ when 'ElicitationResult'
180
+ ElicitationResult.new(data)
181
+ when 'WorktreeCreate'
182
+ WorktreeCreate.new(data)
138
183
  else
139
184
  raise ArgumentError, "Unknown hook type: #{hook_type}"
140
185
  end