claude_hooks 1.0.2 → 1.2.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.
- checksums.yaml +4 -4
- data/.agents/skills/ci-monitoring/SKILL.md +103 -0
- data/.agents/skills/run-tests/SKILL.md +45 -0
- data/.claude/auto-fix.md +7 -0
- data/.claude/settings.local.json +21 -0
- data/AGENTS.md +46 -0
- data/CHANGELOG.md +93 -0
- data/README.md +159 -21
- data/docs/1.1.0_UPGRADE_GUIDE.md +269 -0
- data/docs/API/COMMON.md +7 -0
- 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/NOTIFICATION.md +1 -0
- data/docs/API/PERMISSION_DENIED.md +37 -0
- data/docs/API/PERMISSION_REQUEST.md +199 -0
- data/docs/API/POST_COMPACT.md +30 -0
- data/docs/API/POST_TOOL_BATCH.md +39 -0
- data/docs/API/POST_TOOL_USE.md +1 -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 +9 -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 +2846 -18
- 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/lib/claude_hooks/base.rb +30 -1
- 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/notification.rb +5 -1
- data/lib/claude_hooks/output/base.rb +48 -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 +120 -0
- 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 +26 -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 +63 -0
- 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 +15 -1
- 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 +34 -1
- 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 +42 -0
- metadata +76 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
module Output
|
|
7
|
+
class PostToolBatch < Base
|
|
8
|
+
def decision
|
|
9
|
+
@data['decision']
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def reason
|
|
13
|
+
@data['reason'] || ''
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def blocked?
|
|
17
|
+
decision == 'block'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def additional_context
|
|
21
|
+
hook_specific_output['additionalContext'] || ''
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def exit_code
|
|
25
|
+
default_exit_code
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.merge(*outputs)
|
|
29
|
+
compacted_outputs = outputs.compact
|
|
30
|
+
return compacted_outputs.first if compacted_outputs.length == 1
|
|
31
|
+
return super(*outputs) if compacted_outputs.empty?
|
|
32
|
+
|
|
33
|
+
merged = super(*outputs)
|
|
34
|
+
merged_data = merged.data
|
|
35
|
+
reasons = []
|
|
36
|
+
|
|
37
|
+
compacted_outputs.each do |output|
|
|
38
|
+
output_data = output.respond_to?(:data) ? output.data : output
|
|
39
|
+
if output_data['decision'] == 'block'
|
|
40
|
+
merged_data['decision'] = 'block'
|
|
41
|
+
r = output_data['reason']
|
|
42
|
+
reasons << r if r && !r.empty?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
merged_data['reason'] = reasons.join('; ') if reasons.any?
|
|
47
|
+
new(merged_data)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -19,12 +19,24 @@ module ClaudeHooks
|
|
|
19
19
|
hook_specific_output['additionalContext'] || ''
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def updated_tool_output
|
|
23
|
+
hook_specific_output['updatedToolOutput']
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def updated_mcp_tool_output
|
|
27
|
+
hook_specific_output['updatedMCPToolOutput']
|
|
28
|
+
end
|
|
29
|
+
|
|
22
30
|
# === SEMANTIC HELPERS ===
|
|
23
31
|
|
|
24
32
|
def blocked?
|
|
25
33
|
decision == 'block'
|
|
26
34
|
end
|
|
27
35
|
|
|
36
|
+
def output_updated?
|
|
37
|
+
!updated_tool_output.nil? || !updated_mcp_tool_output.nil?
|
|
38
|
+
end
|
|
39
|
+
|
|
28
40
|
# === EXIT CODE LOGIC ===
|
|
29
41
|
#
|
|
30
42
|
# PostToolUse hooks use the advanced JSON API with exit code 0.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
module Output
|
|
7
|
+
class PostToolUseFailure < Base
|
|
8
|
+
def additional_context
|
|
9
|
+
hook_specific_output['additionalContext'] || ''
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def exit_code
|
|
13
|
+
default_exit_code
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.merge(*outputs)
|
|
17
|
+
compacted_outputs = outputs.compact
|
|
18
|
+
return compacted_outputs.first if compacted_outputs.length == 1
|
|
19
|
+
return super(*outputs) if compacted_outputs.empty?
|
|
20
|
+
|
|
21
|
+
merged = super(*outputs)
|
|
22
|
+
merged_data = merged.data
|
|
23
|
+
contexts = []
|
|
24
|
+
|
|
25
|
+
compacted_outputs.each do |output|
|
|
26
|
+
output_data = output.respond_to?(:data) ? output.data : output
|
|
27
|
+
context = output_data.dig('hookSpecificOutput', 'additionalContext')
|
|
28
|
+
contexts << context if context && !context.empty?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
unless contexts.empty?
|
|
32
|
+
merged_data['hookSpecificOutput'] = {
|
|
33
|
+
'hookEventName' => 'PostToolUseFailure',
|
|
34
|
+
'additionalContext' => contexts.join("\n\n")
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
new(merged_data)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -5,6 +5,20 @@ require_relative 'base'
|
|
|
5
5
|
module ClaudeHooks
|
|
6
6
|
module Output
|
|
7
7
|
class PreCompact < Base
|
|
8
|
+
# === DECISION ACCESSORS ===
|
|
9
|
+
|
|
10
|
+
def decision
|
|
11
|
+
@data['decision']
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reason
|
|
15
|
+
@data['reason'] || ''
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def blocked?
|
|
19
|
+
decision == 'block'
|
|
20
|
+
end
|
|
21
|
+
|
|
8
22
|
def exit_code
|
|
9
23
|
default_exit_code
|
|
10
24
|
end
|
|
@@ -12,9 +26,26 @@ module ClaudeHooks
|
|
|
12
26
|
# === MERGE HELPER ===
|
|
13
27
|
|
|
14
28
|
def self.merge(*outputs)
|
|
29
|
+
compacted_outputs = outputs.compact
|
|
30
|
+
return compacted_outputs.first if compacted_outputs.length == 1
|
|
31
|
+
return super(*outputs) if compacted_outputs.empty?
|
|
32
|
+
|
|
15
33
|
merged = super(*outputs)
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
merged_data = merged.data
|
|
35
|
+
reasons = []
|
|
36
|
+
|
|
37
|
+
compacted_outputs.each do |output|
|
|
38
|
+
output_data = output.respond_to?(:data) ? output.data : output
|
|
39
|
+
if output_data['decision'] == 'block'
|
|
40
|
+
merged_data['decision'] = 'block'
|
|
41
|
+
r = output_data['reason']
|
|
42
|
+
reasons << r if r && !r.empty?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
merged_data['reason'] = reasons.join('; ') if reasons.any?
|
|
47
|
+
new(merged_data)
|
|
48
|
+
end
|
|
18
49
|
end
|
|
19
50
|
end
|
|
20
|
-
end
|
|
51
|
+
end
|
|
@@ -15,6 +15,10 @@ module ClaudeHooks
|
|
|
15
15
|
hook_specific_output['permissionDecisionReason'] || ''
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def updated_input
|
|
19
|
+
hook_specific_output['updatedInput']
|
|
20
|
+
end
|
|
21
|
+
|
|
18
22
|
# === SEMANTIC HELPERS ===
|
|
19
23
|
|
|
20
24
|
def allowed?
|
|
@@ -30,6 +34,14 @@ module ClaudeHooks
|
|
|
30
34
|
permission_decision == 'ask'
|
|
31
35
|
end
|
|
32
36
|
|
|
37
|
+
def input_updated?
|
|
38
|
+
!updated_input.nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def deferred?
|
|
42
|
+
permission_decision == 'defer'
|
|
43
|
+
end
|
|
44
|
+
|
|
33
45
|
# === EXIT CODE LOGIC ===
|
|
34
46
|
#
|
|
35
47
|
# PreToolUse hooks use the advanced JSON API with exit code 0.
|
|
@@ -58,7 +70,7 @@ module ClaudeHooks
|
|
|
58
70
|
merged = super(*outputs)
|
|
59
71
|
merged_data = merged.data
|
|
60
72
|
|
|
61
|
-
# PreToolUse specific merge: deny > ask > allow (most restrictive wins)
|
|
73
|
+
# PreToolUse specific merge: deny > defer > ask > allow (most restrictive wins)
|
|
62
74
|
permission_decision = 'allow'
|
|
63
75
|
permission_reasons = []
|
|
64
76
|
|
|
@@ -71,8 +83,10 @@ module ClaudeHooks
|
|
|
71
83
|
case current_decision
|
|
72
84
|
when 'deny'
|
|
73
85
|
permission_decision = 'deny'
|
|
86
|
+
when 'defer'
|
|
87
|
+
permission_decision = 'defer' unless permission_decision == 'deny'
|
|
74
88
|
when 'ask'
|
|
75
|
-
permission_decision = 'ask' unless permission_decision
|
|
89
|
+
permission_decision = 'ask' unless %w[deny defer].include?(permission_decision)
|
|
76
90
|
end
|
|
77
91
|
|
|
78
92
|
reason = output_data.dig('hookSpecificOutput', 'permissionDecisionReason')
|
|
@@ -87,6 +101,16 @@ module ClaudeHooks
|
|
|
87
101
|
''
|
|
88
102
|
end
|
|
89
103
|
|
|
104
|
+
# If any output has updatedInput, use the last one (most recent wins)
|
|
105
|
+
updated_inputs = []
|
|
106
|
+
compacted_outputs.each do |output|
|
|
107
|
+
output_data = output.respond_to?(:data) ? output.data : output
|
|
108
|
+
updated = output_data.dig('hookSpecificOutput', 'updatedInput')
|
|
109
|
+
updated_inputs << updated if updated
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
merged_data['hookSpecificOutput']['updatedInput'] = updated_inputs.last if updated_inputs.any?
|
|
113
|
+
|
|
90
114
|
new(merged_data)
|
|
91
115
|
end
|
|
92
116
|
end
|
|
@@ -11,6 +11,22 @@ module ClaudeHooks
|
|
|
11
11
|
hook_specific_output['additionalContext'] || ''
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def session_title
|
|
15
|
+
hook_specific_output['sessionTitle']
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initial_user_message
|
|
19
|
+
hook_specific_output['initialUserMessage']
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def watch_paths
|
|
23
|
+
hook_specific_output['watchPaths'] || []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reload_skills?
|
|
27
|
+
hook_specific_output['reloadSkills'] == true
|
|
28
|
+
end
|
|
29
|
+
|
|
14
30
|
# === EXIT CODE LOGIC ===
|
|
15
31
|
|
|
16
32
|
def exit_code
|
|
@@ -27,19 +43,25 @@ module ClaudeHooks
|
|
|
27
43
|
merged = super(*outputs)
|
|
28
44
|
merged_data = merged.data
|
|
29
45
|
contexts = []
|
|
30
|
-
|
|
46
|
+
specific = {}
|
|
47
|
+
|
|
31
48
|
compacted_outputs.each do |output|
|
|
32
49
|
output_data = output.respond_to?(:data) ? output.data : output
|
|
33
|
-
|
|
50
|
+
hso = output_data['hookSpecificOutput'] || {}
|
|
51
|
+
|
|
52
|
+
context = hso['additionalContext']
|
|
34
53
|
contexts << context if context && !context.empty?
|
|
54
|
+
|
|
55
|
+
# Last-non-nil wins for the scalar/array session fields.
|
|
56
|
+
%w[sessionTitle initialUserMessage watchPaths reloadSkills].each do |key|
|
|
57
|
+
specific[key] = hso[key] unless hso[key].nil?
|
|
58
|
+
end
|
|
35
59
|
end
|
|
36
60
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'additionalContext' => contexts.join("\n\n")
|
|
42
|
-
}
|
|
61
|
+
specific['additionalContext'] = contexts.join("\n\n") unless contexts.empty?
|
|
62
|
+
|
|
63
|
+
unless specific.empty?
|
|
64
|
+
merged_data['hookSpecificOutput'] = { 'hookEventName' => 'SessionStart' }.merge(specific)
|
|
43
65
|
end
|
|
44
66
|
|
|
45
67
|
new(merged_data)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
module Output
|
|
7
|
+
class Setup < Base
|
|
8
|
+
def additional_context
|
|
9
|
+
hook_specific_output['additionalContext'] || ''
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def exit_code
|
|
13
|
+
default_exit_code
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.merge(*outputs)
|
|
17
|
+
compacted_outputs = outputs.compact
|
|
18
|
+
return compacted_outputs.first if compacted_outputs.length == 1
|
|
19
|
+
return super(*outputs) if compacted_outputs.empty?
|
|
20
|
+
|
|
21
|
+
merged = super(*outputs)
|
|
22
|
+
merged_data = merged.data
|
|
23
|
+
contexts = []
|
|
24
|
+
|
|
25
|
+
compacted_outputs.each do |output|
|
|
26
|
+
output_data = output.respond_to?(:data) ? output.data : output
|
|
27
|
+
context = output_data.dig('hookSpecificOutput', 'additionalContext')
|
|
28
|
+
contexts << context if context && !context.empty?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
unless contexts.empty?
|
|
32
|
+
merged_data['hookSpecificOutput'] = {
|
|
33
|
+
'hookEventName' => 'Setup',
|
|
34
|
+
'additionalContext' => contexts.join("\n\n")
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
new(merged_data)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -18,6 +18,10 @@ module ClaudeHooks
|
|
|
18
18
|
end
|
|
19
19
|
alias_method :continue_instructions, :reason
|
|
20
20
|
|
|
21
|
+
def additional_context
|
|
22
|
+
hook_specific_output['additionalContext'] || ''
|
|
23
|
+
end
|
|
24
|
+
|
|
21
25
|
# === SEMANTIC HELPERS ===
|
|
22
26
|
|
|
23
27
|
# Check if Claude should be forced to continue (decision == 'block')
|
|
@@ -61,6 +65,7 @@ module ClaudeHooks
|
|
|
61
65
|
|
|
62
66
|
# A blocking reason is actually a "continue instructions"
|
|
63
67
|
blocking_reasons = []
|
|
68
|
+
contexts = []
|
|
64
69
|
|
|
65
70
|
compacted_outputs.each do |output|
|
|
66
71
|
output_data = output.respond_to?(:data) ? output.data : output
|
|
@@ -71,11 +76,21 @@ module ClaudeHooks
|
|
|
71
76
|
reason = output_data['reason']
|
|
72
77
|
blocking_reasons << reason if reason && !reason.empty?
|
|
73
78
|
end
|
|
79
|
+
|
|
80
|
+
context = output_data.dig('hookSpecificOutput', 'additionalContext')
|
|
81
|
+
contexts << context if context && !context.empty?
|
|
74
82
|
end
|
|
75
83
|
|
|
76
84
|
# Combine all blocking reasons / continue instructions
|
|
77
85
|
merged_data['reason'] = blocking_reasons.join('; ') unless blocking_reasons.empty?
|
|
78
86
|
|
|
87
|
+
unless contexts.empty?
|
|
88
|
+
merged_data['hookSpecificOutput'] = {
|
|
89
|
+
'hookEventName' => merged_data.dig('hookSpecificOutput', 'hookEventName') || 'Stop',
|
|
90
|
+
'additionalContext' => contexts.join("\n\n")
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
79
94
|
new(merged_data)
|
|
80
95
|
end
|
|
81
96
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
module Output
|
|
7
|
+
# StopFailure is purely a logging event — output/exit is ignored by Claude Code.
|
|
8
|
+
class StopFailure < Base
|
|
9
|
+
def exit_code
|
|
10
|
+
0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.merge(*outputs)
|
|
14
|
+
merged = super(*outputs)
|
|
15
|
+
new(merged.data)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
module Output
|
|
7
|
+
class SubagentStart < Base
|
|
8
|
+
def additional_context
|
|
9
|
+
hook_specific_output['additionalContext'] || ''
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def exit_code
|
|
13
|
+
default_exit_code
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.merge(*outputs)
|
|
17
|
+
compacted_outputs = outputs.compact
|
|
18
|
+
return compacted_outputs.first if compacted_outputs.length == 1
|
|
19
|
+
return super(*outputs) if compacted_outputs.empty?
|
|
20
|
+
|
|
21
|
+
merged = super(*outputs)
|
|
22
|
+
merged_data = merged.data
|
|
23
|
+
contexts = []
|
|
24
|
+
|
|
25
|
+
compacted_outputs.each do |output|
|
|
26
|
+
output_data = output.respond_to?(:data) ? output.data : output
|
|
27
|
+
context = output_data.dig('hookSpecificOutput', 'additionalContext')
|
|
28
|
+
contexts << context if context && !context.empty?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
unless contexts.empty?
|
|
32
|
+
merged_data['hookSpecificOutput'] = {
|
|
33
|
+
'hookEventName' => 'SubagentStart',
|
|
34
|
+
'additionalContext' => contexts.join("\n\n")
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
new(merged_data)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
module Output
|
|
7
|
+
class TaskCompleted < Base
|
|
8
|
+
def exit_code
|
|
9
|
+
default_exit_code
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.merge(*outputs)
|
|
13
|
+
merged = super(*outputs)
|
|
14
|
+
new(merged.data)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
module Output
|
|
7
|
+
class TaskCreated < Base
|
|
8
|
+
def exit_code
|
|
9
|
+
default_exit_code
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.merge(*outputs)
|
|
13
|
+
merged = super(*outputs)
|
|
14
|
+
new(merged.data)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
module Output
|
|
7
|
+
class TeammateIdle < Base
|
|
8
|
+
def exit_code
|
|
9
|
+
default_exit_code
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.merge(*outputs)
|
|
13
|
+
merged = super(*outputs)
|
|
14
|
+
new(merged.data)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
module Output
|
|
7
|
+
class UserPromptExpansion < Base
|
|
8
|
+
def decision
|
|
9
|
+
@data['decision']
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def reason
|
|
13
|
+
@data['reason'] || ''
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def blocked?
|
|
17
|
+
decision == 'block'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def additional_context
|
|
21
|
+
hook_specific_output['additionalContext'] || ''
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def exit_code
|
|
25
|
+
default_exit_code
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.merge(*outputs)
|
|
29
|
+
compacted_outputs = outputs.compact
|
|
30
|
+
return compacted_outputs.first if compacted_outputs.length == 1
|
|
31
|
+
return super(*outputs) if compacted_outputs.empty?
|
|
32
|
+
|
|
33
|
+
merged = super(*outputs)
|
|
34
|
+
merged_data = merged.data
|
|
35
|
+
reasons = []
|
|
36
|
+
|
|
37
|
+
compacted_outputs.each do |output|
|
|
38
|
+
output_data = output.respond_to?(:data) ? output.data : output
|
|
39
|
+
if output_data['decision'] == 'block'
|
|
40
|
+
merged_data['decision'] = 'block'
|
|
41
|
+
r = output_data['reason']
|
|
42
|
+
reasons << r if r && !r.empty?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
merged_data['reason'] = reasons.join('; ') if reasons.any?
|
|
47
|
+
new(merged_data)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
module Output
|
|
7
|
+
# WorktreeCreate has a special bare-stdout contract:
|
|
8
|
+
# the last non-empty stdout line is interpreted as the worktree path.
|
|
9
|
+
# An empty/missing path causes Claude Code to treat creation as failed.
|
|
10
|
+
class WorktreeCreate < Base
|
|
11
|
+
def worktree_path
|
|
12
|
+
@data['hookSpecificOutput']&.dig('worktreePath') || @data['_worktree_path']
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def exit_code
|
|
16
|
+
worktree_path && !worktree_path.empty? ? 0 : 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def output_stream
|
|
20
|
+
:stdout
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Overrides the default JSON output — prints the bare path instead.
|
|
24
|
+
def output_and_exit
|
|
25
|
+
path = worktree_path
|
|
26
|
+
if path && !path.empty?
|
|
27
|
+
$stdout.puts path
|
|
28
|
+
exit 0
|
|
29
|
+
else
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.merge(*outputs)
|
|
35
|
+
compacted_outputs = outputs.compact
|
|
36
|
+
return compacted_outputs.first if compacted_outputs.length == 1
|
|
37
|
+
return super(*outputs) if compacted_outputs.empty?
|
|
38
|
+
|
|
39
|
+
# Last set path wins
|
|
40
|
+
merged = super(*outputs)
|
|
41
|
+
merged_data = merged.data
|
|
42
|
+
last_path = nil
|
|
43
|
+
|
|
44
|
+
compacted_outputs.each do |output|
|
|
45
|
+
output_data = output.respond_to?(:data) ? output.data : output
|
|
46
|
+
p = output_data.dig('hookSpecificOutput', 'worktreePath') || output_data['_worktree_path']
|
|
47
|
+
last_path = p if p && !p.empty?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if last_path
|
|
51
|
+
merged_data['hookSpecificOutput'] ||= { 'hookEventName' => 'WorktreeCreate' }
|
|
52
|
+
merged_data['hookSpecificOutput']['worktreePath'] = last_path
|
|
53
|
+
merged_data['_worktree_path'] = last_path
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
new(merged_data)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
module Output
|
|
7
|
+
# WorktreeRemove is non-blocking — only suppressOutput is honored.
|
|
8
|
+
class WorktreeRemove < Base
|
|
9
|
+
def exit_code
|
|
10
|
+
0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.merge(*outputs)
|
|
14
|
+
merged = super(*outputs)
|
|
15
|
+
new(merged.data)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
class PermissionDenied < Base
|
|
7
|
+
def self.hook_type
|
|
8
|
+
'PermissionDenied'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.input_fields
|
|
12
|
+
%w[tool_name tool_input tool_use_id reason]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tool_name
|
|
16
|
+
@input_data['tool_name']
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def tool_input
|
|
20
|
+
@input_data['tool_input']
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def tool_use_id
|
|
24
|
+
@input_data['tool_use_id'] || @input_data['toolUseId']
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reason
|
|
28
|
+
@input_data['reason']
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def retry!
|
|
32
|
+
@output_data['hookSpecificOutput'] ||= { 'hookEventName' => hook_event_name }
|
|
33
|
+
@output_data['hookSpecificOutput']['retry'] = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def no_retry!
|
|
37
|
+
@output_data['hookSpecificOutput'] ||= { 'hookEventName' => hook_event_name }
|
|
38
|
+
@output_data['hookSpecificOutput']['retry'] = false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|