claude_hooks 0.1.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 +7 -0
- data/CHANGELOG.md +31 -0
- data/README.md +777 -0
- data/WHY.md +65 -0
- data/claude_hooks.gemspec +39 -0
- data/example_dotclaude/commands/.gitkeep +0 -0
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit/append_rules.rb +66 -0
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit/log_user_prompt.rb +50 -0
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit.rb +44 -0
- data/example_dotclaude/hooks/handlers/user_prompt_submit/append_rules.rb +66 -0
- data/example_dotclaude/hooks/handlers/user_prompt_submit/log_user_prompt.rb +50 -0
- data/example_dotclaude/settings.json +23 -0
- data/lib/claude_hooks/base.rb +157 -0
- data/lib/claude_hooks/configuration.rb +145 -0
- data/lib/claude_hooks/logger.rb +66 -0
- data/lib/claude_hooks/notification.rb +22 -0
- data/lib/claude_hooks/post_tool_use.rb +55 -0
- data/lib/claude_hooks/pre_compact.rb +42 -0
- data/lib/claude_hooks/pre_tool_use.rb +88 -0
- data/lib/claude_hooks/session_start.rb +58 -0
- data/lib/claude_hooks/stop.rb +61 -0
- data/lib/claude_hooks/subagent_stop.rb +11 -0
- data/lib/claude_hooks/user_prompt_submit.rb +73 -0
- data/lib/claude_hooks/version.rb +5 -0
- data/lib/claude_hooks.rb +18 -0
- metadata +115 -0
data/WHY.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Why is this DSL useful?
|
2
|
+
|
3
|
+
When creating fairly complex hook systems for Claude Code it is easy to end up either with:
|
4
|
+
- Fairly monolithic scripts that becomes hard to maintain and have to handle complex merging logic for the output.
|
5
|
+
- A lot of scripts set up in the `settings.json` that all use the same hook input / output logic that needs to be rewritten multiple times.
|
6
|
+
|
7
|
+
Furthermore, Claude's documentation on hooks is a bit hard to navigate and setting up hooks is not necessarily intuitive or straightforward having to play around with STDIN, STDOUT, STDERR, exit codes, etc.
|
8
|
+
|
9
|
+
This DSL enables the easy creation of composable, testable handlers that can be easily maintained and reused and is suited for both a monolithic approach (using entrypoints and handlers) or a modular one (using handlers only).
|
10
|
+
|
11
|
+
```bash
|
12
|
+
# Monolithic approach (Recommended)
|
13
|
+
settings.json
|
14
|
+
└── [On PreToolUse Hook] -> entrypoints/pre_tool_use.rb
|
15
|
+
├── calls handlers/pre_tool_use/danger_check.rb
|
16
|
+
├── calls handlers/pre_tool_use/audit_logger.rb
|
17
|
+
└── calls handlers/pre_tool_use/permission_guard.rb
|
18
|
+
|
19
|
+
# Modular approach
|
20
|
+
settings.json
|
21
|
+
├── [On PreToolUse Hook] -> handlers/pre_tool_use/danger_check.rb
|
22
|
+
├── [On PreToolUse Hook] -> handlers/pre_tool_use/audit_logger.rb
|
23
|
+
└── [On PreToolUse Hook] -> handlers/pre_tool_use/permission_guard.rb
|
24
|
+
```
|
25
|
+
|
26
|
+
## For both approaches it will bring
|
27
|
+
|
28
|
+
1. Normalized Input/Output handling - input parsing, validation, straightforward output helpers (block_tool!, add_context!).
|
29
|
+
|
30
|
+
2. Hook-Specific APIs - 8 different hook types with tailored methods (e.g., ask_for_permission! vs block_tool!) and smart merge logic for combining outputs.
|
31
|
+
|
32
|
+
3. Session-Based Logging - Dedicated logger to understand the flow of what happens in Claude Code and write it out to a `session-{session_id}.log` file.
|
33
|
+
|
34
|
+
4. Configuration Management - Centralized config and helpers for use across the hook system.
|
35
|
+
|
36
|
+
5. Testing Support - Standalone execution mode for individual hook testing and CLI testing with sample JSON input.
|
37
|
+
|
38
|
+
|
39
|
+
## For a monolithic approach it will additionally bring
|
40
|
+
|
41
|
+
1. Composable Hook Handlers
|
42
|
+
For instance, `entrypoints/user_prompt_submit.rb` orchestrates multiple handlers in one place:
|
43
|
+
```ruby
|
44
|
+
# Add contextual rules
|
45
|
+
append_rules_result = AppendRules.new(input_data).call
|
46
|
+
# Audit logging
|
47
|
+
log_result = LogUserPrompt.new(input_data).call
|
48
|
+
|
49
|
+
# Merge outputs to Claude Code
|
50
|
+
puts ClaudeHooks::UserPromptSubmit.merge_outputs(append_rules_result, log_result)
|
51
|
+
```
|
52
|
+
|
53
|
+
2. Intelligent Output Merging
|
54
|
+
- Each hook handler can return different decisions (block, context additions, etc.)
|
55
|
+
- The framework intelligently merges conflicting decisions (e.g., any handler can block)
|
56
|
+
- Combines multiple contexts cleanly
|
57
|
+
|
58
|
+
3. Individual Handler Testability
|
59
|
+
Each handler can run standalone for testing:
|
60
|
+
```ruby
|
61
|
+
if __FILE__ == $0
|
62
|
+
hook = AppendRules.new(JSON.parse(STDIN.read))
|
63
|
+
puts hook.stringify_output
|
64
|
+
end
|
65
|
+
```
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/claude_hooks/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "claude_hooks"
|
7
|
+
spec.version = ClaudeHooks::VERSION
|
8
|
+
spec.authors = ["Gabriel Dehan"]
|
9
|
+
spec.email = ["dehan.gabriel@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Ruby DSL for creating Claude Code hooks"
|
12
|
+
spec.description = "A Ruby DSL framework for creating Claude Code hooks with composable hook scripts that enable teams to easily implement logging, security checks, and workflow automation."
|
13
|
+
spec.homepage = "https://github.com/gabriel-dehan/claude_hooks"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.7.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
20
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
# Runtime dependencies
|
34
|
+
spec.add_dependency "json", "~> 2.0"
|
35
|
+
|
36
|
+
# Development dependencies
|
37
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
38
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
39
|
+
end
|
File without changes
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'claude_hooks'
|
4
|
+
|
5
|
+
# Hook script that appends rules to user prompt
|
6
|
+
class AppendRules < ClaudeHooks::UserPromptSubmit
|
7
|
+
|
8
|
+
def call
|
9
|
+
log "Executing AppendRules hook"
|
10
|
+
|
11
|
+
# Read the rule content
|
12
|
+
rule_content = read_rule_content
|
13
|
+
|
14
|
+
if rule_content
|
15
|
+
add_additional_context!(rule_content)
|
16
|
+
log "Successfully added rule content as additional context (#{rule_content.length} characters)"
|
17
|
+
else
|
18
|
+
log "No rule content found", level: :warn
|
19
|
+
end
|
20
|
+
|
21
|
+
output_data
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def read_rule_content
|
27
|
+
rule_file_path = path_for('rules/post-user-prompt.rule.md')
|
28
|
+
|
29
|
+
if File.exist?(rule_file_path)
|
30
|
+
content = File.read(rule_file_path).strip
|
31
|
+
return content unless content.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
log "Rule file not found or empty at: #{rule_file_path}", level: :warn
|
35
|
+
log "Base directory: #{base_dir}"
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# If this file is run directly (for testing), call the hook script
|
41
|
+
if __FILE__ == $0
|
42
|
+
begin
|
43
|
+
require 'json'
|
44
|
+
|
45
|
+
input_data = JSON.parse(STDIN.read)
|
46
|
+
hook = AppendRules.new(input_data)
|
47
|
+
hook.call
|
48
|
+
puts hook.stringify_output
|
49
|
+
rescue JSON::ParserError => e
|
50
|
+
STDERR.puts "Error parsing JSON: #{e.message}"
|
51
|
+
puts JSON.generate({
|
52
|
+
continue: false,
|
53
|
+
stopReason: "JSON parsing error in AppendRules: #{e.message}",
|
54
|
+
suppressOutput: false
|
55
|
+
})
|
56
|
+
exit 0
|
57
|
+
rescue StandardError => e
|
58
|
+
STDERR.puts "Error in AppendRules hook: #{e.message}, #{e.backtrace.join("\n")}"
|
59
|
+
puts JSON.generate({
|
60
|
+
continue: false,
|
61
|
+
stopReason: "AppendRules execution error: #{e.message}",
|
62
|
+
suppressOutput: false
|
63
|
+
})
|
64
|
+
exit 0
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'claude_hooks'
|
5
|
+
|
6
|
+
# Example hook module that logs user prompts to a file
|
7
|
+
class LogUserPrompt < ClaudeHooks::UserPromptSubmit
|
8
|
+
|
9
|
+
def call
|
10
|
+
log "Executing LogUserPrompt hook"
|
11
|
+
|
12
|
+
# Log the prompt to a file (just as an example)
|
13
|
+
log_file_path = path_for('logs/user_prompts.log')
|
14
|
+
ensure_log_directory_exists
|
15
|
+
|
16
|
+
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
17
|
+
|
18
|
+
log <<~TEXT
|
19
|
+
Prompt: #{current_prompt}
|
20
|
+
Logged user prompt to #{log_file_path}
|
21
|
+
TEXT
|
22
|
+
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def ensure_log_directory_exists
|
29
|
+
log_dir = path_for('logs')
|
30
|
+
FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# If this file is run directly (for testing), call the hook
|
35
|
+
if __FILE__ == $0
|
36
|
+
begin
|
37
|
+
require 'json'
|
38
|
+
|
39
|
+
hook = LogUserPrompt.new(JSON.parse(STDIN.read))
|
40
|
+
hook.call
|
41
|
+
rescue StandardError => e
|
42
|
+
STDERR.puts "Error in LogUserPrompt hook: #{e.message}, #{e.backtrace.join("\n")}"
|
43
|
+
puts JSON.generate({
|
44
|
+
continue: false,
|
45
|
+
stopReason: "LogUserPrompt execution error: #{e.message}",
|
46
|
+
suppressOutput: false
|
47
|
+
})
|
48
|
+
exit 0
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'claude_hooks'
|
4
|
+
require 'json'
|
5
|
+
require_relative '../user_prompt_submit/append_rules'
|
6
|
+
require_relative '../user_prompt_submit/log_user_prompt'
|
7
|
+
|
8
|
+
begin
|
9
|
+
# Read input from stdin
|
10
|
+
input_data = JSON.parse(STDIN.read)
|
11
|
+
|
12
|
+
# Execute all hook scripts
|
13
|
+
append_rules = AppendRules.new(input_data)
|
14
|
+
appended_rules_result = append_rules.call
|
15
|
+
|
16
|
+
log_user_prompt = LogUserPrompt.new(input_data)
|
17
|
+
log_user_prompt_result = log_user_prompt.call
|
18
|
+
|
19
|
+
# Merge all hook results intelligently using the UserPromptSubmit class method
|
20
|
+
hook_output = ClaudeHooks::UserPromptSubmit.merge_outputs(appended_rules_result, log_user_prompt_result)
|
21
|
+
|
22
|
+
# Output final merged result to Claude Code
|
23
|
+
puts JSON.generate(hook_output)
|
24
|
+
|
25
|
+
exit 0
|
26
|
+
rescue JSON::ParserError => e
|
27
|
+
STDERR.puts "Error parsing JSON: #{e.message}"
|
28
|
+
|
29
|
+
puts JSON.generate({
|
30
|
+
continue: false,
|
31
|
+
stopReason: "Hook JSON parsing error: #{e.message}",
|
32
|
+
suppressOutput: false
|
33
|
+
})
|
34
|
+
exit 1
|
35
|
+
rescue StandardError => e
|
36
|
+
STDERR.puts "Error in UserPromptSubmit hook: #{e.message} #{e.backtrace.join("\n")}"
|
37
|
+
|
38
|
+
puts JSON.generate({
|
39
|
+
continue: false,
|
40
|
+
stopReason: "Hook execution error: #{e.message} #{e.backtrace.join("\n")}",
|
41
|
+
suppressOutput: false
|
42
|
+
})
|
43
|
+
exit 1
|
44
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'claude_hooks'
|
4
|
+
|
5
|
+
# Hook script that appends rules to user prompt
|
6
|
+
class AppendRules < ClaudeHooks::UserPromptSubmit
|
7
|
+
|
8
|
+
def call
|
9
|
+
log "Executing AppendRules hook"
|
10
|
+
|
11
|
+
# Read the rule content
|
12
|
+
rule_content = read_rule_content
|
13
|
+
|
14
|
+
if rule_content
|
15
|
+
add_additional_context!(rule_content)
|
16
|
+
log "Successfully added rule content as additional context (#{rule_content.length} characters)"
|
17
|
+
else
|
18
|
+
log "No rule content found", level: :warn
|
19
|
+
end
|
20
|
+
|
21
|
+
output_data
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def read_rule_content
|
27
|
+
rule_file_path = path_for('rules/post-user-prompt.rule.md')
|
28
|
+
|
29
|
+
if File.exist?(rule_file_path)
|
30
|
+
content = File.read(rule_file_path).strip
|
31
|
+
return content unless content.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
log "Rule file not found or empty at: #{rule_file_path}", level: :warn
|
35
|
+
log "Base directory: #{base_dir}"
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# If this file is run directly (for testing), call the hook script
|
41
|
+
if __FILE__ == $0
|
42
|
+
begin
|
43
|
+
require 'json'
|
44
|
+
|
45
|
+
input_data = JSON.parse(STDIN.read)
|
46
|
+
hook = AppendRules.new(input_data)
|
47
|
+
hook.call
|
48
|
+
puts hook.stringify_output
|
49
|
+
rescue JSON::ParserError => e
|
50
|
+
STDERR.puts "Error parsing JSON: #{e.message}"
|
51
|
+
puts JSON.generate({
|
52
|
+
continue: false,
|
53
|
+
stopReason: "JSON parsing error in AppendRules: #{e.message}",
|
54
|
+
suppressOutput: false
|
55
|
+
})
|
56
|
+
exit 0
|
57
|
+
rescue StandardError => e
|
58
|
+
STDERR.puts "Error in AppendRules hook: #{e.message}, #{e.backtrace.join("\n")}"
|
59
|
+
puts JSON.generate({
|
60
|
+
continue: false,
|
61
|
+
stopReason: "AppendRules execution error: #{e.message}",
|
62
|
+
suppressOutput: false
|
63
|
+
})
|
64
|
+
exit 0
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'claude_hooks'
|
5
|
+
|
6
|
+
# Example hook module that logs user prompts to a file
|
7
|
+
class LogUserPrompt < ClaudeHooks::UserPromptSubmit
|
8
|
+
|
9
|
+
def call
|
10
|
+
log "Executing LogUserPrompt hook"
|
11
|
+
|
12
|
+
# Log the prompt to a file (just as an example)
|
13
|
+
log_file_path = path_for('logs/user_prompts.log')
|
14
|
+
ensure_log_directory_exists
|
15
|
+
|
16
|
+
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
17
|
+
|
18
|
+
log <<~TEXT
|
19
|
+
Prompt: #{current_prompt}
|
20
|
+
Logged user prompt to #{log_file_path}
|
21
|
+
TEXT
|
22
|
+
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def ensure_log_directory_exists
|
29
|
+
log_dir = path_for('logs')
|
30
|
+
FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# If this file is run directly (for testing), call the hook
|
35
|
+
if __FILE__ == $0
|
36
|
+
begin
|
37
|
+
require 'json'
|
38
|
+
|
39
|
+
hook = LogUserPrompt.new(JSON.parse(STDIN.read))
|
40
|
+
hook.call
|
41
|
+
rescue StandardError => e
|
42
|
+
STDERR.puts "Error in LogUserPrompt hook: #{e.message}, #{e.backtrace.join("\n")}"
|
43
|
+
puts JSON.generate({
|
44
|
+
continue: false,
|
45
|
+
stopReason: "LogUserPrompt execution error: #{e.message}",
|
46
|
+
suppressOutput: false
|
47
|
+
})
|
48
|
+
exit 0
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
{
|
2
|
+
"includeCoAuthoredBy": false,
|
3
|
+
"permissions": {
|
4
|
+
"allow": []
|
5
|
+
},
|
6
|
+
"hooks": {
|
7
|
+
"UserPromptSubmit": [
|
8
|
+
{
|
9
|
+
"matcher": "",
|
10
|
+
"hooks": [
|
11
|
+
{
|
12
|
+
"type": "command",
|
13
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/entrypoints/user_prompt_submit.rb"
|
14
|
+
}
|
15
|
+
]
|
16
|
+
}
|
17
|
+
]
|
18
|
+
},
|
19
|
+
"env": {
|
20
|
+
"CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": "1",
|
21
|
+
"DISABLE_TELEMETRY": "1"
|
22
|
+
}
|
23
|
+
}
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require_relative 'configuration'
|
5
|
+
require_relative 'logger'
|
6
|
+
|
7
|
+
module ClaudeHooks
|
8
|
+
# Base class for Claude Code hook scripts
|
9
|
+
class Base
|
10
|
+
# Common input fields for all hook types
|
11
|
+
COMMON_INPUT_FIELDS = %w[session_id transcript_path cwd hook_event_name].freeze
|
12
|
+
|
13
|
+
# Override in subclasses to specify hook type
|
14
|
+
def self.hook_type
|
15
|
+
raise NotImplementedError, "Subclasses must define hook_type"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Override in subclasses to specify hook-specific input fields
|
19
|
+
def self.input_fields
|
20
|
+
raise NotImplementedError, "Subclasses must define input_fields"
|
21
|
+
end
|
22
|
+
|
23
|
+
def hook_type
|
24
|
+
self.class.hook_type
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :config, :input_data, :output_data, :logger
|
28
|
+
def initialize(input_data = {})
|
29
|
+
@config = Configuration.config
|
30
|
+
@input_data = input_data
|
31
|
+
@output_data = {
|
32
|
+
'continue' => true,
|
33
|
+
'stopReason' => '',
|
34
|
+
'suppressOutput' => false
|
35
|
+
}
|
36
|
+
@logger = Logger.new(session_id, self.class.name)
|
37
|
+
|
38
|
+
validate_input!
|
39
|
+
end
|
40
|
+
|
41
|
+
# Main execution method - override in subclasses
|
42
|
+
def call
|
43
|
+
raise NotImplementedError, "Subclasses must implement the call method"
|
44
|
+
end
|
45
|
+
|
46
|
+
def stringify_output
|
47
|
+
JSON.generate(@output_data)
|
48
|
+
end
|
49
|
+
|
50
|
+
# === COMMON INPUT DATA ACCESS ===
|
51
|
+
|
52
|
+
def session_id
|
53
|
+
@input_data['session_id'] || 'claude-default-session'
|
54
|
+
end
|
55
|
+
|
56
|
+
def transcript_path
|
57
|
+
@input_data['transcript_path']
|
58
|
+
end
|
59
|
+
|
60
|
+
def cwd
|
61
|
+
@input_data['cwd']
|
62
|
+
end
|
63
|
+
|
64
|
+
def hook_event_name
|
65
|
+
@input_data['hook_event_name'] || @input_data['hookEventName'] || hook_type
|
66
|
+
end
|
67
|
+
|
68
|
+
def read_transcript
|
69
|
+
unless transcript_path && File.exist?(transcript_path)
|
70
|
+
log "Transcript file not found at #{transcript_path}", level: :warn
|
71
|
+
return ''
|
72
|
+
end
|
73
|
+
|
74
|
+
begin
|
75
|
+
File.read(transcript_path)
|
76
|
+
rescue => e
|
77
|
+
log "Error reading transcript file at #{transcript_path}: #{e.message}", level: :error
|
78
|
+
''
|
79
|
+
end
|
80
|
+
end
|
81
|
+
alias_method :transcript, :read_transcript
|
82
|
+
|
83
|
+
# === COMMON OUTPUT HELPERS ===
|
84
|
+
|
85
|
+
# Allow Claude to continue (default: true)
|
86
|
+
def allow_continue!
|
87
|
+
@output_data['continue'] = true
|
88
|
+
end
|
89
|
+
|
90
|
+
def prevent_continue!(reason)
|
91
|
+
@output_data['continue'] = false
|
92
|
+
@output_data['stopReason'] = reason
|
93
|
+
end
|
94
|
+
|
95
|
+
# Hide stdout from transcript mode (default: false)
|
96
|
+
def suppress_output!
|
97
|
+
@output_data['suppressOutput'] = true
|
98
|
+
end
|
99
|
+
|
100
|
+
def show_output!
|
101
|
+
@output_data['suppressOutput'] = false
|
102
|
+
end
|
103
|
+
|
104
|
+
def clear_specifics!
|
105
|
+
@output_data['hookSpecificOutput'] = nil
|
106
|
+
end
|
107
|
+
|
108
|
+
# === CONFIG AND UTILITY METHODS ===
|
109
|
+
|
110
|
+
def base_dir
|
111
|
+
Configuration.base_dir
|
112
|
+
end
|
113
|
+
|
114
|
+
def path_for(relative_path)
|
115
|
+
Configuration.path_for(relative_path)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Supports both single messages and blocks for multiline logging
|
119
|
+
def log(message = nil, level: :info, &block)
|
120
|
+
@logger.log(message, level: level, &block)
|
121
|
+
end
|
122
|
+
|
123
|
+
protected
|
124
|
+
|
125
|
+
# === MERGE HELPER BASE ===
|
126
|
+
|
127
|
+
# Handles common merging logic for all hook types
|
128
|
+
# Subclasses should call super and add their specific logic
|
129
|
+
def self.merge_outputs(*outputs_data)
|
130
|
+
compacted_outputs_data = outputs_data.compact
|
131
|
+
return { 'continue' => true, 'stopReason' => '', 'suppressOutput' => false } if compacted_outputs_data.empty?
|
132
|
+
|
133
|
+
# Initialize merged result with defaults
|
134
|
+
merged = { 'continue' => true, 'stopReason' => '', 'suppressOutput' => false }
|
135
|
+
|
136
|
+
# Apply common merge logic
|
137
|
+
compacted_outputs_data.each do |output|
|
138
|
+
merged['continue'] = false if output['continue'] == false
|
139
|
+
merged['stopReason'] = [merged['stopReason'], output['stopReason']].compact.reject(&:empty?).join('; ')
|
140
|
+
merged['suppressOutput'] = true if output['suppressOutput'] == true
|
141
|
+
end
|
142
|
+
|
143
|
+
merged
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def validate_input!
|
149
|
+
expected_fields = COMMON_INPUT_FIELDS + self.class.input_fields
|
150
|
+
missing_fields = expected_fields - @input_data.keys
|
151
|
+
|
152
|
+
unless missing_fields.empty?
|
153
|
+
log "Missing required input fields for #{hook_type}: #{missing_fields.join(', ')}", level: :warn
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|