claude_hooks 1.1.0 → 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/settings.local.json +21 -0
- data/AGENTS.md +46 -0
- data/CHANGELOG.md +43 -0
- data/README.md +156 -21
- 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/lib/claude_hooks/base.rb +25 -0
- 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 -2
|
@@ -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
|
|
@@ -38,6 +38,10 @@ module ClaudeHooks
|
|
|
38
38
|
!updated_input.nil?
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def deferred?
|
|
42
|
+
permission_decision == 'defer'
|
|
43
|
+
end
|
|
44
|
+
|
|
41
45
|
# === EXIT CODE LOGIC ===
|
|
42
46
|
#
|
|
43
47
|
# PreToolUse hooks use the advanced JSON API with exit code 0.
|
|
@@ -66,7 +70,7 @@ module ClaudeHooks
|
|
|
66
70
|
merged = super(*outputs)
|
|
67
71
|
merged_data = merged.data
|
|
68
72
|
|
|
69
|
-
# PreToolUse specific merge: deny > ask > allow (most restrictive wins)
|
|
73
|
+
# PreToolUse specific merge: deny > defer > ask > allow (most restrictive wins)
|
|
70
74
|
permission_decision = 'allow'
|
|
71
75
|
permission_reasons = []
|
|
72
76
|
|
|
@@ -79,8 +83,10 @@ module ClaudeHooks
|
|
|
79
83
|
case current_decision
|
|
80
84
|
when 'deny'
|
|
81
85
|
permission_decision = 'deny'
|
|
86
|
+
when 'defer'
|
|
87
|
+
permission_decision = 'defer' unless permission_decision == 'deny'
|
|
82
88
|
when 'ask'
|
|
83
|
-
permission_decision = 'ask' unless permission_decision
|
|
89
|
+
permission_decision = 'ask' unless %w[deny defer].include?(permission_decision)
|
|
84
90
|
end
|
|
85
91
|
|
|
86
92
|
reason = output_data.dig('hookSpecificOutput', 'permissionDecisionReason')
|
|
@@ -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
|
|
@@ -26,30 +26,37 @@ module ClaudeHooks
|
|
|
26
26
|
@input_data['tool_use_id'] || @input_data['toolUseId']
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
def permission_suggestions
|
|
30
|
+
@input_data['permissionSuggestions'] || @input_data['permission_suggestions'] || []
|
|
31
|
+
end
|
|
32
|
+
|
|
29
33
|
# === OUTPUT DATA HELPERS ===
|
|
30
34
|
|
|
31
35
|
def allow_permission!(reason = '')
|
|
32
36
|
@output_data['hookSpecificOutput'] = {
|
|
33
37
|
'hookEventName' => hook_event_name,
|
|
34
|
-
'
|
|
35
|
-
'permissionDecisionReason' => reason
|
|
38
|
+
'decision' => { 'behavior' => 'allow' }
|
|
36
39
|
}
|
|
40
|
+
@output_data['hookSpecificOutput']['decision']['message'] = reason unless reason.empty?
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
def deny_permission!(reason = '')
|
|
43
|
+
def deny_permission!(reason = '', interrupt: nil)
|
|
44
|
+
decision = { 'behavior' => 'deny' }
|
|
45
|
+
decision['message'] = reason unless reason.empty?
|
|
46
|
+
decision['interrupt'] = interrupt unless interrupt.nil?
|
|
40
47
|
@output_data['hookSpecificOutput'] = {
|
|
41
48
|
'hookEventName' => hook_event_name,
|
|
42
|
-
'
|
|
43
|
-
'permissionDecisionReason' => reason
|
|
49
|
+
'decision' => decision
|
|
44
50
|
}
|
|
45
51
|
end
|
|
46
52
|
|
|
47
|
-
def update_input_and_allow!(updated_input, reason = '')
|
|
53
|
+
def update_input_and_allow!(updated_input, reason = '', updated_permissions: nil)
|
|
54
|
+
decision = { 'behavior' => 'allow', 'updatedInput' => updated_input }
|
|
55
|
+
decision['message'] = reason unless reason.empty?
|
|
56
|
+
decision['updatedPermissions'] = updated_permissions unless updated_permissions.nil?
|
|
48
57
|
@output_data['hookSpecificOutput'] = {
|
|
49
58
|
'hookEventName' => hook_event_name,
|
|
50
|
-
'
|
|
51
|
-
'permissionDecisionReason' => reason,
|
|
52
|
-
'updatedInput' => updated_input
|
|
59
|
+
'decision' => decision
|
|
53
60
|
}
|
|
54
61
|
end
|
|
55
62
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
class PostCompact < Base
|
|
7
|
+
def self.hook_type
|
|
8
|
+
'PostCompact'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.input_fields
|
|
12
|
+
%w[trigger compact_summary]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def trigger
|
|
16
|
+
@input_data['trigger']
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def compact_summary
|
|
20
|
+
@input_data['compact_summary'] || @input_data['compactSummary']
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|