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.
- checksums.yaml +4 -4
- data/.agents/skills/ci-monitoring/SKILL.md +103 -0
- data/.agents/skills/run-tests/SKILL.md +45 -0
- data/AGENTS.md +46 -0
- data/CHANGELOG.md +64 -0
- data/README.md +303 -239
- data/docs/1.0.0_MIGRATION_GUIDE.md +6 -7
- data/docs/API/COMMON.md +7 -1
- data/docs/API/CONFIG_CHANGE.md +39 -0
- data/docs/API/CWD_CHANGED.md +34 -0
- data/docs/API/ELICITATION.md +44 -0
- data/docs/API/ELICITATION_RESULT.md +43 -0
- data/docs/API/FILE_CHANGED.md +37 -0
- data/docs/API/INSTRUCTIONS_LOADED.md +30 -0
- data/docs/API/MESSAGE_DISPLAY.md +38 -0
- data/docs/API/PERMISSION_DENIED.md +37 -0
- data/docs/API/PERMISSION_REQUEST.md +11 -8
- data/docs/API/POST_COMPACT.md +30 -0
- data/docs/API/POST_TOOL_BATCH.md +39 -0
- data/docs/API/POST_TOOL_USE_FAILURE.md +31 -0
- data/docs/API/PRE_COMPACT.md +10 -2
- data/docs/API/PRE_TOOL_USE.md +5 -1
- data/docs/API/SETUP.md +37 -0
- data/docs/API/STOP_FAILURE.md +19 -0
- data/docs/API/SUBAGENT_START.md +40 -0
- data/docs/API/TASK_COMPLETED.md +43 -0
- data/docs/API/TASK_CREATED.md +43 -0
- data/docs/API/TEAMMATE_IDLE.md +40 -0
- data/docs/API/USER_PROMPT_EXPANSION.md +43 -0
- data/docs/API/WORKTREE_CREATE.md +39 -0
- data/docs/API/WORKTREE_REMOVE.md +33 -0
- data/docs/PROMPT_BASED_HOOKS.md +386 -0
- data/docs/external/claude-hooks-reference.md +2406 -854
- data/docs/mitts/ideabox.md +12 -0
- data/docs/mitts/prd.md +417 -0
- data/docs/mitts/setup.md +303 -0
- data/docs/mitts/task.md +44 -0
- data/example_dotclaude/hooks/entrypoints/session_end.rb +5 -22
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit.rb +4 -26
- data/example_dotclaude/hooks/{handlers/pre_tool_use/github_guard.rb → github_guard.rb} +7 -13
- data/example_dotclaude/settings.json +1 -1
- data/lib/claude_hooks/base.rb +25 -0
- data/lib/claude_hooks/cli.rb +84 -149
- data/lib/claude_hooks/config_change.rb +28 -0
- data/lib/claude_hooks/cwd_changed.rb +28 -0
- data/lib/claude_hooks/elicitation.rb +61 -0
- data/lib/claude_hooks/elicitation_result.rb +57 -0
- data/lib/claude_hooks/file_changed.rb +41 -0
- data/lib/claude_hooks/instructions_loaded.rb +23 -0
- data/lib/claude_hooks/message_display.rb +58 -0
- data/lib/claude_hooks/output/base.rb +46 -1
- data/lib/claude_hooks/output/config_change.rb +47 -0
- data/lib/claude_hooks/output/cwd_changed.rb +42 -0
- data/lib/claude_hooks/output/elicitation.rb +67 -0
- data/lib/claude_hooks/output/elicitation_result.rb +63 -0
- data/lib/claude_hooks/output/file_changed.rb +42 -0
- data/lib/claude_hooks/output/instructions_loaded.rb +19 -0
- data/lib/claude_hooks/output/message_display.rb +49 -0
- data/lib/claude_hooks/output/permission_denied.rb +42 -0
- data/lib/claude_hooks/output/permission_request.rb +50 -25
- data/lib/claude_hooks/output/post_compact.rb +18 -0
- data/lib/claude_hooks/output/post_tool_batch.rb +51 -0
- data/lib/claude_hooks/output/post_tool_use.rb +12 -0
- data/lib/claude_hooks/output/post_tool_use_failure.rb +42 -0
- data/lib/claude_hooks/output/pre_compact.rb +34 -3
- data/lib/claude_hooks/output/pre_tool_use.rb +8 -2
- data/lib/claude_hooks/output/session_start.rb +30 -8
- data/lib/claude_hooks/output/setup.rb +42 -0
- data/lib/claude_hooks/output/stop.rb +15 -0
- data/lib/claude_hooks/output/stop_failure.rb +19 -0
- data/lib/claude_hooks/output/subagent_start.rb +42 -0
- data/lib/claude_hooks/output/task_completed.rb +18 -0
- data/lib/claude_hooks/output/task_created.rb +18 -0
- data/lib/claude_hooks/output/teammate_idle.rb +18 -0
- data/lib/claude_hooks/output/user_prompt_expansion.rb +51 -0
- data/lib/claude_hooks/output/worktree_create.rb +60 -0
- data/lib/claude_hooks/output/worktree_remove.rb +19 -0
- data/lib/claude_hooks/permission_denied.rb +41 -0
- data/lib/claude_hooks/permission_request.rb +16 -9
- data/lib/claude_hooks/post_compact.rb +23 -0
- data/lib/claude_hooks/post_tool_batch.rb +59 -0
- data/lib/claude_hooks/post_tool_use.rb +10 -0
- data/lib/claude_hooks/post_tool_use_failure.rb +45 -0
- data/lib/claude_hooks/pre_compact.rb +7 -0
- data/lib/claude_hooks/pre_tool_use.rb +20 -2
- data/lib/claude_hooks/session_start.rb +28 -0
- data/lib/claude_hooks/setup.rb +25 -0
- data/lib/claude_hooks/stop.rb +17 -0
- data/lib/claude_hooks/stop_failure.rb +27 -0
- data/lib/claude_hooks/subagent_start.rb +23 -0
- data/lib/claude_hooks/subagent_stop.rb +5 -1
- data/lib/claude_hooks/task_completed.rb +35 -0
- data/lib/claude_hooks/task_created.rb +35 -0
- data/lib/claude_hooks/teammate_idle.rb +23 -0
- data/lib/claude_hooks/user_prompt_expansion.rb +45 -0
- data/lib/claude_hooks/version.rb +1 -1
- data/lib/claude_hooks/worktree_create.rb +27 -0
- data/lib/claude_hooks/worktree_remove.rb +19 -0
- data/lib/claude_hooks.rb +40 -0
- metadata +71 -4
- data/example_dotclaude/hooks/entrypoints/pre_tool_use.rb +0 -25
data/lib/claude_hooks/cli.rb
CHANGED
|
@@ -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
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
47
|
+
handle_run_error("Hook execution error: #{e.message}", on_error, backtrace: e.backtrace)
|
|
39
48
|
end
|
|
40
49
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
#
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|