Class: ClaudeHooks::Output::Stop

Inherits:
Base
  • Object
show all
Defined in:
lib/claude_hooks/output/stop.rb

Overview

Note: In Stop hooks, 'decision: block' actually means "force Claude to continue" This is counterintuitive but matches Claude Code's expected behavior

Direct Known Subclasses

SubagentStop

Instance Attribute Summary

Attributes inherited from Base

#data

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#continue?, for_hook_type, #hook_specific_output, #initialize, #output_and_exit, #stop_reason, #suppress_output?, #system_message, #terminal_sequence, #to_json

Constructor Details

This class inherits a constructor from ClaudeHooks::Output::Base

Class Method Details

.merge(*outputs) ⇒ Object

MERGE HELPER ===



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/claude_hooks/output/stop.rb', line 58

def self.merge(*outputs)
  compacted_outputs = outputs.compact
  return compacted_outputs.first if compacted_outputs.length == 1
  return super(*outputs) if compacted_outputs.empty?
  
  merged = super(*outputs)
  merged_data = merged.data

  # A blocking reason is actually a "continue instructions"
  blocking_reasons = []
  contexts = []

  compacted_outputs.each do |output|
    output_data = output.respond_to?(:data) ? output.data : output

    # Handle decision - if any hook says 'block', respect that
    if output_data['decision'] == 'block'
      merged_data['decision'] = 'block'
      reason = output_data['reason']
      blocking_reasons << reason if reason && !reason.empty?
    end

    context = output_data.dig('hookSpecificOutput', 'additionalContext')
    contexts << context if context && !context.empty?
  end

  # Combine all blocking reasons / continue instructions
  merged_data['reason'] = blocking_reasons.join('; ') unless blocking_reasons.empty?

  unless contexts.empty?
    merged_data['hookSpecificOutput'] = {
      'hookEventName' => merged_data.dig('hookSpecificOutput', 'hookEventName') || 'Stop',
      'additionalContext' => contexts.join("\n\n")
    }
  end

  new(merged_data)
end

Instance Method Details

#additional_contextObject



21
22
23
# File 'lib/claude_hooks/output/stop.rb', line 21

def additional_context
  hook_specific_output['additionalContext'] || ''
end

#decisionObject

DECISION ACCESSORS ===



12
13
14
# File 'lib/claude_hooks/output/stop.rb', line 12

def decision
  @data['decision']
end

#exit_codeObject

EXIT CODE LOGIC ===

Stop hooks use the advanced JSON API with exit code 0. Per Anthropic guidance: when using structured JSON with decision/reason fields, always output to stdout with exit 0 (not stderr with exit 2). Reference: https://github.com/anthropics/claude-code/issues/10875



44
45
46
# File 'lib/claude_hooks/output/stop.rb', line 44

def exit_code
  0
end

#output_streamObject

OUTPUT STREAM LOGIC ===

Stop hooks always output to stdout when using the JSON API. This follows the same pattern as PreToolUse hooks.



52
53
54
# File 'lib/claude_hooks/output/stop.rb', line 52

def output_stream
  :stdout
end

#reasonObject Also known as: continue_instructions



16
17
18
# File 'lib/claude_hooks/output/stop.rb', line 16

def reason
  @data['reason'] || ''
end

#should_continue?Boolean

Check if Claude should be forced to continue (decision == 'block') Note: 'block' in Stop hooks means "block the stopping", i.e., continue

Returns:

  • (Boolean)


29
30
31
# File 'lib/claude_hooks/output/stop.rb', line 29

def should_continue?
  decision == 'block'
end

#should_stop?Boolean

Check if Claude should stop normally (decision != 'block')

Returns:

  • (Boolean)


34
35
36
# File 'lib/claude_hooks/output/stop.rb', line 34

def should_stop?
  decision != 'block'
end