claude_hooks 1.0.2 → 1.1.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.
@@ -9,7 +9,7 @@ module ClaudeHooks
9
9
  # Base class for Claude Code hook scripts
10
10
  class Base
11
11
  # Common input fields for all hook types
12
- COMMON_INPUT_FIELDS = %w[session_id transcript_path cwd hook_event_name].freeze
12
+ COMMON_INPUT_FIELDS = %w[session_id transcript_path cwd hook_event_name permission_mode].freeze
13
13
 
14
14
  # Override in subclasses to specify hook type
15
15
  def self.hook_type
@@ -71,6 +71,10 @@ module ClaudeHooks
71
71
  @input_data['hook_event_name'] || @input_data['hookEventName'] || hook_type
72
72
  end
73
73
 
74
+ def permission_mode
75
+ @input_data['permission_mode'] || @input_data['permissionMode'] || 'default'
76
+ end
77
+
74
78
  def read_transcript
75
79
  unless transcript_path && File.exist?(transcript_path)
76
80
  log "Transcript file not found at #{transcript_path}", level: :warn
@@ -9,7 +9,7 @@ module ClaudeHooks
9
9
  end
10
10
 
11
11
  def self.input_fields
12
- %w[message]
12
+ %w[message notification_type]
13
13
  end
14
14
 
15
15
  # === INPUT DATA ACCESS ===
@@ -18,5 +18,9 @@ module ClaudeHooks
18
18
  @input_data['message']
19
19
  end
20
20
  alias_method :notification_message, :message
21
+
22
+ def notification_type
23
+ @input_data['notification_type'] || @input_data['notificationType']
24
+ end
21
25
  end
22
26
  end
@@ -119,6 +119,8 @@ module ClaudeHooks
119
119
  UserPromptSubmit.new(data)
120
120
  when 'PreToolUse'
121
121
  PreToolUse.new(data)
122
+ when 'PermissionRequest'
123
+ PermissionRequest.new(data)
122
124
  when 'PostToolUse'
123
125
  PostToolUse.new(data)
124
126
  when 'Stop'
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module ClaudeHooks
6
+ module Output
7
+ class PermissionRequest < Base
8
+ # === PERMISSION DECISION ACCESSORS ===
9
+
10
+ def permission_decision
11
+ hook_specific_output['permissionDecision']
12
+ end
13
+
14
+ def permission_reason
15
+ hook_specific_output['permissionDecisionReason'] || ''
16
+ end
17
+
18
+ def updated_input
19
+ hook_specific_output['updatedInput']
20
+ end
21
+
22
+ # === SEMANTIC HELPERS ===
23
+
24
+ def allowed?
25
+ permission_decision == 'allow'
26
+ end
27
+
28
+ def denied?
29
+ permission_decision == 'deny'
30
+ end
31
+
32
+ def input_updated?
33
+ !updated_input.nil?
34
+ end
35
+
36
+ # === EXIT CODE LOGIC ===
37
+ #
38
+ # PermissionRequest hooks use the advanced JSON API with exit code 0.
39
+ # Following the same pattern as PreToolUse (permission-based hooks).
40
+ def exit_code
41
+ 0
42
+ end
43
+
44
+ # === OUTPUT STREAM LOGIC ===
45
+ #
46
+ # PermissionRequest hooks always output to stdout when using the JSON API.
47
+ def output_stream
48
+ :stdout
49
+ end
50
+
51
+ # === MERGE HELPER ===
52
+
53
+ def self.merge(*outputs)
54
+ compacted_outputs = outputs.compact
55
+ return compacted_outputs.first if compacted_outputs.length == 1
56
+ return super(*outputs) if compacted_outputs.empty?
57
+
58
+ merged = super(*outputs)
59
+ merged_data = merged.data
60
+
61
+ # PermissionRequest specific merge: deny > allow (most restrictive wins)
62
+ permission_decision = 'allow'
63
+ permission_reasons = []
64
+ updated_inputs = []
65
+
66
+ compacted_outputs.each do |output|
67
+ output_data = output.respond_to?(:data) ? output.data : output
68
+
69
+ next unless output_data.dig('hookSpecificOutput', 'permissionDecision')
70
+
71
+ current_decision = output_data['hookSpecificOutput']['permissionDecision']
72
+ permission_decision = 'deny' if current_decision == 'deny'
73
+
74
+ reason = output_data.dig('hookSpecificOutput', 'permissionDecisionReason')
75
+ permission_reasons << reason if reason && !reason.empty?
76
+
77
+ updated = output_data.dig('hookSpecificOutput', 'updatedInput')
78
+ updated_inputs << updated if updated
79
+ end
80
+
81
+ merged_data['hookSpecificOutput'] ||= { 'hookEventName' => 'PermissionRequest' }
82
+ merged_data['hookSpecificOutput']['permissionDecision'] = permission_decision
83
+ merged_data['hookSpecificOutput']['permissionDecisionReason'] = if permission_reasons.any?
84
+ permission_reasons.join('; ')
85
+ else
86
+ ''
87
+ end
88
+ # Last updated input wins
89
+ merged_data['hookSpecificOutput']['updatedInput'] = updated_inputs.last if updated_inputs.any?
90
+
91
+ new(merged_data)
92
+ end
93
+ end
94
+ end
95
+ 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,10 @@ module ClaudeHooks
30
34
  permission_decision == 'ask'
31
35
  end
32
36
 
37
+ def input_updated?
38
+ !updated_input.nil?
39
+ end
40
+
33
41
  # === EXIT CODE LOGIC ===
34
42
  #
35
43
  # PreToolUse hooks use the advanced JSON API with exit code 0.
@@ -87,6 +95,16 @@ module ClaudeHooks
87
95
  ''
88
96
  end
89
97
 
98
+ # If any output has updatedInput, use the last one (most recent wins)
99
+ updated_inputs = []
100
+ compacted_outputs.each do |output|
101
+ output_data = output.respond_to?(:data) ? output.data : output
102
+ updated = output_data.dig('hookSpecificOutput', 'updatedInput')
103
+ updated_inputs << updated if updated
104
+ end
105
+
106
+ merged_data['hookSpecificOutput']['updatedInput'] = updated_inputs.last if updated_inputs.any?
107
+
90
108
  new(merged_data)
91
109
  end
92
110
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module ClaudeHooks
6
+ class PermissionRequest < Base
7
+ def self.hook_type
8
+ 'PermissionRequest'
9
+ end
10
+
11
+ def self.input_fields
12
+ %w[tool_name tool_input tool_use_id]
13
+ end
14
+
15
+ # === INPUT DATA ACCESS ===
16
+
17
+ def tool_name
18
+ @input_data['tool_name']
19
+ end
20
+
21
+ def tool_input
22
+ @input_data['tool_input']
23
+ end
24
+
25
+ def tool_use_id
26
+ @input_data['tool_use_id'] || @input_data['toolUseId']
27
+ end
28
+
29
+ # === OUTPUT DATA HELPERS ===
30
+
31
+ def allow_permission!(reason = '')
32
+ @output_data['hookSpecificOutput'] = {
33
+ 'hookEventName' => hook_event_name,
34
+ 'permissionDecision' => 'allow',
35
+ 'permissionDecisionReason' => reason
36
+ }
37
+ end
38
+
39
+ def deny_permission!(reason = '')
40
+ @output_data['hookSpecificOutput'] = {
41
+ 'hookEventName' => hook_event_name,
42
+ 'permissionDecision' => 'deny',
43
+ 'permissionDecisionReason' => reason
44
+ }
45
+ end
46
+
47
+ def update_input_and_allow!(updated_input, reason = '')
48
+ @output_data['hookSpecificOutput'] = {
49
+ 'hookEventName' => hook_event_name,
50
+ 'permissionDecision' => 'allow',
51
+ 'permissionDecisionReason' => reason,
52
+ 'updatedInput' => updated_input
53
+ }
54
+ end
55
+ end
56
+ end
@@ -9,7 +9,7 @@ module ClaudeHooks
9
9
  end
10
10
 
11
11
  def self.input_fields
12
- %w[tool_name tool_input tool_response]
12
+ %w[tool_name tool_input tool_response tool_use_id]
13
13
  end
14
14
 
15
15
  # === INPUT DATA ACCESS ===
@@ -26,6 +26,10 @@ module ClaudeHooks
26
26
  @input_data['tool_response']
27
27
  end
28
28
 
29
+ def tool_use_id
30
+ @input_data['tool_use_id'] || @input_data['toolUseId']
31
+ end
32
+
29
33
  # === OUTPUT DATA HELPERS ===
30
34
 
31
35
  def block_tool!(reason = '')
@@ -9,7 +9,7 @@ module ClaudeHooks
9
9
  end
10
10
 
11
11
  def self.input_fields
12
- %w[tool_name tool_input]
12
+ %w[tool_name tool_input tool_use_id]
13
13
  end
14
14
 
15
15
  # === INPUT DATA ACCESS ===
@@ -22,6 +22,10 @@ module ClaudeHooks
22
22
  @input_data['tool_input']
23
23
  end
24
24
 
25
+ def tool_use_id
26
+ @input_data['tool_use_id'] || @input_data['toolUseId']
27
+ end
28
+
25
29
  # === OUTPUT DATA HELPERS ===
26
30
 
27
31
  def approve_tool!(reason = '')
@@ -47,5 +51,16 @@ module ClaudeHooks
47
51
  'permissionDecisionReason' => reason
48
52
  }
49
53
  end
54
+
55
+ def update_tool_input!(updated_input)
56
+ @output_data['hookSpecificOutput'] ||= {
57
+ 'hookEventName' => hook_event_name,
58
+ 'permissionDecision' => 'allow'
59
+ }
60
+ @output_data['hookSpecificOutput']['updatedInput'] = updated_input
61
+
62
+ # Ensure permission decision is 'allow' when updating input
63
+ @output_data['hookSpecificOutput']['permissionDecision'] = 'allow'
64
+ end
50
65
  end
51
66
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeHooks
4
- VERSION = "1.0.2"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/claude_hooks.rb CHANGED
@@ -9,6 +9,7 @@ require_relative "claude_hooks/cli"
9
9
  # Hook classes
10
10
  require_relative "claude_hooks/user_prompt_submit"
11
11
  require_relative "claude_hooks/pre_tool_use"
12
+ require_relative "claude_hooks/permission_request"
12
13
  require_relative "claude_hooks/post_tool_use"
13
14
  require_relative "claude_hooks/notification"
14
15
  require_relative "claude_hooks/stop"
@@ -21,6 +22,7 @@ require_relative "claude_hooks/session_end"
21
22
  require_relative "claude_hooks/output/base"
22
23
  require_relative "claude_hooks/output/user_prompt_submit"
23
24
  require_relative "claude_hooks/output/pre_tool_use"
25
+ require_relative "claude_hooks/output/permission_request"
24
26
  require_relative "claude_hooks/output/post_tool_use"
25
27
  require_relative "claude_hooks/output/notification"
26
28
  require_relative "claude_hooks/output/stop"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude_hooks
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Dehan
@@ -61,12 +61,15 @@ executables: []
61
61
  extensions: []
62
62
  extra_rdoc_files: []
63
63
  files:
64
+ - ".claude/auto-fix.md"
64
65
  - CHANGELOG.md
65
66
  - README.md
66
67
  - claude_hooks.gemspec
67
68
  - docs/1.0.0_MIGRATION_GUIDE.md
69
+ - docs/1.1.0_UPGRADE_GUIDE.md
68
70
  - docs/API/COMMON.md
69
71
  - docs/API/NOTIFICATION.md
72
+ - docs/API/PERMISSION_REQUEST.md
70
73
  - docs/API/POST_TOOL_USE.md
71
74
  - docs/API/PRE_COMPACT.md
72
75
  - docs/API/PRE_TOOL_USE.md
@@ -96,6 +99,7 @@ files:
96
99
  - lib/claude_hooks/notification.rb
97
100
  - lib/claude_hooks/output/base.rb
98
101
  - lib/claude_hooks/output/notification.rb
102
+ - lib/claude_hooks/output/permission_request.rb
99
103
  - lib/claude_hooks/output/post_tool_use.rb
100
104
  - lib/claude_hooks/output/pre_compact.rb
101
105
  - lib/claude_hooks/output/pre_tool_use.rb
@@ -104,6 +108,7 @@ files:
104
108
  - lib/claude_hooks/output/stop.rb
105
109
  - lib/claude_hooks/output/subagent_stop.rb
106
110
  - lib/claude_hooks/output/user_prompt_submit.rb
111
+ - lib/claude_hooks/permission_request.rb
107
112
  - lib/claude_hooks/post_tool_use.rb
108
113
  - lib/claude_hooks/pre_compact.rb
109
114
  - lib/claude_hooks/pre_tool_use.rb