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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/skills/ci-monitoring/SKILL.md +103 -0
  3. data/.agents/skills/run-tests/SKILL.md +45 -0
  4. data/.claude/settings.local.json +21 -0
  5. data/AGENTS.md +46 -0
  6. data/CHANGELOG.md +43 -0
  7. data/README.md +156 -21
  8. data/docs/API/COMMON.md +7 -1
  9. data/docs/API/CONFIG_CHANGE.md +39 -0
  10. data/docs/API/CWD_CHANGED.md +34 -0
  11. data/docs/API/ELICITATION.md +44 -0
  12. data/docs/API/ELICITATION_RESULT.md +43 -0
  13. data/docs/API/FILE_CHANGED.md +37 -0
  14. data/docs/API/INSTRUCTIONS_LOADED.md +30 -0
  15. data/docs/API/MESSAGE_DISPLAY.md +38 -0
  16. data/docs/API/PERMISSION_DENIED.md +37 -0
  17. data/docs/API/PERMISSION_REQUEST.md +11 -8
  18. data/docs/API/POST_COMPACT.md +30 -0
  19. data/docs/API/POST_TOOL_BATCH.md +39 -0
  20. data/docs/API/POST_TOOL_USE_FAILURE.md +31 -0
  21. data/docs/API/PRE_COMPACT.md +10 -2
  22. data/docs/API/PRE_TOOL_USE.md +5 -1
  23. data/docs/API/SETUP.md +37 -0
  24. data/docs/API/STOP_FAILURE.md +19 -0
  25. data/docs/API/SUBAGENT_START.md +40 -0
  26. data/docs/API/TASK_COMPLETED.md +43 -0
  27. data/docs/API/TASK_CREATED.md +43 -0
  28. data/docs/API/TEAMMATE_IDLE.md +40 -0
  29. data/docs/API/USER_PROMPT_EXPANSION.md +43 -0
  30. data/docs/API/WORKTREE_CREATE.md +39 -0
  31. data/docs/API/WORKTREE_REMOVE.md +33 -0
  32. data/docs/PROMPT_BASED_HOOKS.md +386 -0
  33. data/docs/external/claude-hooks-reference.md +2406 -854
  34. data/docs/mitts/ideabox.md +12 -0
  35. data/docs/mitts/prd.md +417 -0
  36. data/docs/mitts/setup.md +303 -0
  37. data/docs/mitts/task.md +44 -0
  38. data/lib/claude_hooks/base.rb +25 -0
  39. data/lib/claude_hooks/config_change.rb +28 -0
  40. data/lib/claude_hooks/cwd_changed.rb +28 -0
  41. data/lib/claude_hooks/elicitation.rb +61 -0
  42. data/lib/claude_hooks/elicitation_result.rb +57 -0
  43. data/lib/claude_hooks/file_changed.rb +41 -0
  44. data/lib/claude_hooks/instructions_loaded.rb +23 -0
  45. data/lib/claude_hooks/message_display.rb +58 -0
  46. data/lib/claude_hooks/output/base.rb +46 -1
  47. data/lib/claude_hooks/output/config_change.rb +47 -0
  48. data/lib/claude_hooks/output/cwd_changed.rb +42 -0
  49. data/lib/claude_hooks/output/elicitation.rb +67 -0
  50. data/lib/claude_hooks/output/elicitation_result.rb +63 -0
  51. data/lib/claude_hooks/output/file_changed.rb +42 -0
  52. data/lib/claude_hooks/output/instructions_loaded.rb +19 -0
  53. data/lib/claude_hooks/output/message_display.rb +49 -0
  54. data/lib/claude_hooks/output/permission_denied.rb +42 -0
  55. data/lib/claude_hooks/output/permission_request.rb +50 -25
  56. data/lib/claude_hooks/output/post_compact.rb +18 -0
  57. data/lib/claude_hooks/output/post_tool_batch.rb +51 -0
  58. data/lib/claude_hooks/output/post_tool_use.rb +12 -0
  59. data/lib/claude_hooks/output/post_tool_use_failure.rb +42 -0
  60. data/lib/claude_hooks/output/pre_compact.rb +34 -3
  61. data/lib/claude_hooks/output/pre_tool_use.rb +8 -2
  62. data/lib/claude_hooks/output/session_start.rb +30 -8
  63. data/lib/claude_hooks/output/setup.rb +42 -0
  64. data/lib/claude_hooks/output/stop.rb +15 -0
  65. data/lib/claude_hooks/output/stop_failure.rb +19 -0
  66. data/lib/claude_hooks/output/subagent_start.rb +42 -0
  67. data/lib/claude_hooks/output/task_completed.rb +18 -0
  68. data/lib/claude_hooks/output/task_created.rb +18 -0
  69. data/lib/claude_hooks/output/teammate_idle.rb +18 -0
  70. data/lib/claude_hooks/output/user_prompt_expansion.rb +51 -0
  71. data/lib/claude_hooks/output/worktree_create.rb +60 -0
  72. data/lib/claude_hooks/output/worktree_remove.rb +19 -0
  73. data/lib/claude_hooks/permission_denied.rb +41 -0
  74. data/lib/claude_hooks/permission_request.rb +16 -9
  75. data/lib/claude_hooks/post_compact.rb +23 -0
  76. data/lib/claude_hooks/post_tool_batch.rb +59 -0
  77. data/lib/claude_hooks/post_tool_use.rb +10 -0
  78. data/lib/claude_hooks/post_tool_use_failure.rb +45 -0
  79. data/lib/claude_hooks/pre_compact.rb +7 -0
  80. data/lib/claude_hooks/pre_tool_use.rb +20 -2
  81. data/lib/claude_hooks/session_start.rb +28 -0
  82. data/lib/claude_hooks/setup.rb +25 -0
  83. data/lib/claude_hooks/stop.rb +17 -0
  84. data/lib/claude_hooks/stop_failure.rb +27 -0
  85. data/lib/claude_hooks/subagent_start.rb +23 -0
  86. data/lib/claude_hooks/subagent_stop.rb +5 -1
  87. data/lib/claude_hooks/task_completed.rb +35 -0
  88. data/lib/claude_hooks/task_created.rb +35 -0
  89. data/lib/claude_hooks/teammate_idle.rb +23 -0
  90. data/lib/claude_hooks/user_prompt_expansion.rb +45 -0
  91. data/lib/claude_hooks/version.rb +1 -1
  92. data/lib/claude_hooks/worktree_create.rb +27 -0
  93. data/lib/claude_hooks/worktree_remove.rb +19 -0
  94. data/lib/claude_hooks.rb +40 -0
  95. 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
- new(merged.data)
17
- end
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 == 'deny'
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
- context = output_data.dig('hookSpecificOutput', 'additionalContext')
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
- # Set merged additional context
38
- unless contexts.empty?
39
- merged_data['hookSpecificOutput'] = {
40
- 'hookEventName' => 'SessionStart',
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
- 'permissionDecision' => 'allow',
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
- 'permissionDecision' => 'deny',
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
- 'permissionDecision' => 'allow',
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