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.
- checksums.yaml +4 -4
- data/.claude/auto-fix.md +7 -0
- data/CHANGELOG.md +50 -0
- data/README.md +7 -4
- data/docs/1.1.0_UPGRADE_GUIDE.md +269 -0
- data/docs/API/COMMON.md +1 -0
- data/docs/API/NOTIFICATION.md +1 -0
- data/docs/API/PERMISSION_REQUEST.md +196 -0
- data/docs/API/POST_TOOL_USE.md +1 -0
- data/docs/API/PRE_TOOL_USE.md +4 -0
- data/docs/external/claude-hooks-reference.md +1294 -18
- data/lib/claude_hooks/base.rb +5 -1
- data/lib/claude_hooks/notification.rb +5 -1
- data/lib/claude_hooks/output/base.rb +2 -0
- data/lib/claude_hooks/output/permission_request.rb +95 -0
- data/lib/claude_hooks/output/pre_tool_use.rb +18 -0
- data/lib/claude_hooks/permission_request.rb +56 -0
- data/lib/claude_hooks/post_tool_use.rb +5 -1
- data/lib/claude_hooks/pre_tool_use.rb +16 -1
- data/lib/claude_hooks/version.rb +1 -1
- data/lib/claude_hooks.rb +2 -0
- metadata +6 -1
data/lib/claude_hooks/base.rb
CHANGED
|
@@ -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
|
|
@@ -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
|
data/lib/claude_hooks/version.rb
CHANGED
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
|
|
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
|