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
|
@@ -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
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
class PostToolBatch < Base
|
|
7
|
+
def self.hook_type
|
|
8
|
+
'PostToolBatch'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.input_fields
|
|
12
|
+
%w[tool_calls]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tool_calls
|
|
16
|
+
@input_data['tool_calls'] || @input_data['toolCalls'] || []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Convenience partitions over tool_calls. Entries carry a `tool_response`
|
|
20
|
+
# rather than an explicit success flag, so success is derived from the
|
|
21
|
+
# presence of a response that isn't flagged as an error. When your payload
|
|
22
|
+
# shape differs, use the raw `tool_calls` array instead.
|
|
23
|
+
def succeeded_calls
|
|
24
|
+
tool_calls.reject { |call| failed_call?(call) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def failed_calls
|
|
28
|
+
tool_calls.select { |call| failed_call?(call) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def block!(reason = '')
|
|
32
|
+
@output_data['decision'] = 'block'
|
|
33
|
+
@output_data['reason'] = reason
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_additional_context!(context)
|
|
37
|
+
@output_data['hookSpecificOutput'] ||= { 'hookEventName' => hook_event_name }
|
|
38
|
+
@output_data['hookSpecificOutput']['additionalContext'] = context
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# A call is considered failed when its response is missing or carries an
|
|
44
|
+
# error marker (`is_error`/`isError` true, or an `error` key).
|
|
45
|
+
def failed_call?(call)
|
|
46
|
+
return true unless call.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
response = call['tool_response'] || call['toolResponse']
|
|
49
|
+
return true if response.nil?
|
|
50
|
+
|
|
51
|
+
if response.is_a?(Hash)
|
|
52
|
+
return true if response['is_error'] == true || response['isError'] == true
|
|
53
|
+
return true if response.key?('error') && !response['error'].nil?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -49,5 +49,15 @@ module ClaudeHooks
|
|
|
49
49
|
}
|
|
50
50
|
end
|
|
51
51
|
alias_method :add_context!, :add_additional_context!
|
|
52
|
+
|
|
53
|
+
def update_tool_output!(value)
|
|
54
|
+
@output_data['hookSpecificOutput'] ||= { 'hookEventName' => hook_event_name }
|
|
55
|
+
@output_data['hookSpecificOutput']['updatedToolOutput'] = value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def update_mcp_tool_output!(value)
|
|
59
|
+
@output_data['hookSpecificOutput'] ||= { 'hookEventName' => hook_event_name }
|
|
60
|
+
@output_data['hookSpecificOutput']['updatedMCPToolOutput'] = value
|
|
61
|
+
end
|
|
52
62
|
end
|
|
53
63
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module ClaudeHooks
|
|
6
|
+
class PostToolUseFailure < Base
|
|
7
|
+
def self.hook_type
|
|
8
|
+
'PostToolUseFailure'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.input_fields
|
|
12
|
+
%w[tool_name tool_input tool_use_id error]
|
|
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 error
|
|
28
|
+
@input_data['error']
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def is_interrupt
|
|
32
|
+
@input_data.key?('is_interrupt') ? @input_data['is_interrupt'] : @input_data['isInterrupt']
|
|
33
|
+
end
|
|
34
|
+
alias_method :interrupt?, :is_interrupt
|
|
35
|
+
|
|
36
|
+
def duration_ms
|
|
37
|
+
@input_data['duration_ms'] || @input_data['durationMs']
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def add_additional_context!(context)
|
|
41
|
+
@output_data['hookSpecificOutput'] ||= { 'hookEventName' => hook_event_name }
|
|
42
|
+
@output_data['hookSpecificOutput']['additionalContext'] = context
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -27,6 +27,13 @@ module ClaudeHooks
|
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# === OUTPUT DATA HELPERS ===
|
|
31
|
+
|
|
32
|
+
def block!(reason = '')
|
|
33
|
+
@output_data['decision'] = 'block'
|
|
34
|
+
@output_data['reason'] = reason
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
# === UTILITY HELPERS ===
|
|
31
38
|
|
|
32
39
|
def backup_transcript!(backup_file_path)
|