claude_hooks 0.2.1 → 1.0.2

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +137 -0
  3. data/README.md +287 -356
  4. data/claude_hooks.gemspec +2 -2
  5. data/docs/1.0.0_MIGRATION_GUIDE.md +228 -0
  6. data/docs/API/COMMON.md +83 -0
  7. data/docs/API/NOTIFICATION.md +32 -0
  8. data/docs/API/POST_TOOL_USE.md +45 -0
  9. data/docs/API/PRE_COMPACT.md +39 -0
  10. data/docs/API/PRE_TOOL_USE.md +110 -0
  11. data/docs/API/SESSION_END.md +100 -0
  12. data/docs/API/SESSION_START.md +40 -0
  13. data/docs/API/STOP.md +47 -0
  14. data/docs/API/SUBAGENT_STOP.md +47 -0
  15. data/docs/API/USER_PROMPT_SUBMIT.md +47 -0
  16. data/{WHY.md → docs/WHY.md} +15 -8
  17. data/docs/external/claude-hooks-reference.md +34 -0
  18. data/example_dotclaude/hooks/entrypoints/pre_tool_use.rb +25 -0
  19. data/example_dotclaude/hooks/entrypoints/session_end.rb +35 -0
  20. data/example_dotclaude/hooks/entrypoints/user_prompt_submit.rb +17 -12
  21. data/example_dotclaude/hooks/handlers/pre_tool_use/github_guard.rb +253 -0
  22. data/example_dotclaude/hooks/handlers/session_end/cleanup_handler.rb +55 -0
  23. data/example_dotclaude/hooks/handlers/session_end/log_session_stats.rb +64 -0
  24. data/example_dotclaude/hooks/handlers/user_prompt_submit/append_rules.rb +3 -2
  25. data/example_dotclaude/hooks/handlers/user_prompt_submit/log_user_prompt.rb +2 -2
  26. data/example_dotclaude/plugins/README.md +175 -0
  27. data/example_dotclaude/settings.json +22 -0
  28. data/lib/claude_hooks/base.rb +16 -24
  29. data/lib/claude_hooks/cli.rb +75 -1
  30. data/lib/claude_hooks/logger.rb +0 -1
  31. data/lib/claude_hooks/output/base.rb +152 -0
  32. data/lib/claude_hooks/output/notification.rb +22 -0
  33. data/lib/claude_hooks/output/post_tool_use.rb +76 -0
  34. data/lib/claude_hooks/output/pre_compact.rb +20 -0
  35. data/lib/claude_hooks/output/pre_tool_use.rb +94 -0
  36. data/lib/claude_hooks/output/session_end.rb +24 -0
  37. data/lib/claude_hooks/output/session_start.rb +49 -0
  38. data/lib/claude_hooks/output/stop.rb +83 -0
  39. data/lib/claude_hooks/output/subagent_stop.rb +14 -0
  40. data/lib/claude_hooks/output/user_prompt_submit.rb +78 -0
  41. data/lib/claude_hooks/post_tool_use.rb +6 -12
  42. data/lib/claude_hooks/pre_tool_use.rb +0 -37
  43. data/lib/claude_hooks/session_end.rb +43 -0
  44. data/lib/claude_hooks/session_start.rb +0 -23
  45. data/lib/claude_hooks/stop.rb +8 -25
  46. data/lib/claude_hooks/user_prompt_submit.rb +0 -26
  47. data/lib/claude_hooks/version.rb +1 -1
  48. data/lib/claude_hooks.rb +15 -0
  49. metadata +37 -8
data/claude_hooks.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
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
13
  spec.homepage = "https://github.com/gabriel-dehan/claude_hooks"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.7.0"
15
+ spec.required_ruby_version = ">= 3.2.0"
16
16
 
17
17
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
18
  spec.metadata["homepage_uri"] = spec.homepage
@@ -35,5 +35,5 @@ Gem::Specification.new do |spec|
35
35
 
36
36
  # Development dependencies
37
37
  spec.add_development_dependency "rake", "~> 13.0"
38
- spec.add_development_dependency "rspec", "~> 3.0"
38
+ spec.add_development_dependency "minitest", "~> 5.0"
39
39
  end
@@ -0,0 +1,228 @@
1
+ # Migration Guide: 1.0.0
2
+
3
+ This guide shows how to migrate from the old manual exit code patterns to the new simplified way introduced in v1.0.0.
4
+
5
+ ## Problem: The Old Way (Before)
6
+
7
+ ```ruby
8
+ #!/usr/bin/env ruby
9
+
10
+ require 'claude_hooks'
11
+ require_relative '../handlers/pre_tool_use/github_guard'
12
+
13
+ begin
14
+ # Read Claude Code input from stdin
15
+ input_data = JSON.parse(STDIN.read)
16
+
17
+ github_guard = GithubGuard.new(input_data)
18
+ guard_output = github_guard.call
19
+
20
+ # MANUAL: Extract hook-specific decision
21
+ hook_specific_decision = guard_output['hookSpecificOutput']['permissionDecision']
22
+ is_allowed_to_continue = guard_output['continue'] != false
23
+ final_output = github_guard.stringify_output
24
+
25
+ # MANUAL: Complex exit logic that you need to understand and get right
26
+ if is_allowed_to_continue
27
+ case hook_specific_decision
28
+ when 'block'
29
+ STDERR.puts final_output
30
+ exit 1
31
+ when 'ask'
32
+ STDERR.puts final_output
33
+ exit 2
34
+ else
35
+ puts final_output
36
+ exit 0
37
+ end
38
+ else
39
+ STDERR.puts final_output
40
+ exit 1
41
+ end
42
+ rescue StandardError => e
43
+ # MANUAL: Error handling with manual JSON generation and stream selection
44
+ puts JSON.generate({
45
+ continue: false,
46
+ stopReason: "PreToolUser Hook execution error: #{e.message}",
47
+ suppressOutput: false
48
+ })
49
+ exit 1 # Allow anyway to not block developers if there is an issue
50
+ end
51
+ ```
52
+
53
+ **Problems with this approach:**
54
+ - A lot of lines of repetitive boilerplate
55
+ - Need to understand Claude Code's internal exit code semantics
56
+ - Manual stream selection (STDOUT vs STDERR)
57
+ - Error-prone - easy to get exit codes wrong
58
+ - Need to manually extract hook-specific data from nested hashes
59
+
60
+ ## Solution: The New Way (After)
61
+
62
+ ```ruby
63
+ #!/usr/bin/env ruby
64
+
65
+ require 'claude_hooks'
66
+ require_relative '../handlers/pre_tool_use/github_guard'
67
+
68
+ begin
69
+ # Read Claude Code input from stdin
70
+ input_data = JSON.parse(STDIN.read)
71
+
72
+ github_guard = GithubGuard.new(input_data)
73
+ github_guard.call
74
+
75
+ # NEW: One line handles everything!
76
+ github_guard.output_and_exit
77
+
78
+ rescue StandardError => e
79
+ # NEW: Simple error handling
80
+ STDERR.puts JSON.generate({
81
+ continue: false,
82
+ stopReason: "Hook execution error: #{e.message}",
83
+ suppressOutput: false
84
+ })
85
+ # Non-blocking error
86
+ exit 1
87
+ end
88
+ ```
89
+
90
+ **Benefits:**
91
+ - **More concise+** - 70% less boilerplate code
92
+ - **No Claude Code knowledge needed** - just use the gem's API
93
+ - **Automatic exit codes** based on hook-specific logic
94
+ - **Automatic stream selection** (STDOUT/STDERR). Note: `PreToolUse` always outputs to STDOUT.
95
+ - **Type-safe access** to hook-specific fields
96
+
97
+ ## New Output Object API
98
+
99
+ ### PreToolUse Hooks
100
+
101
+ ```ruby
102
+ hook = GithubGuard.new(input_data)
103
+ hook.call
104
+ output = hook.output
105
+
106
+ # Clean semantic access
107
+ puts "Permission: #{output.permission_decision}" # 'allow', 'deny', or 'ask'
108
+ puts "Reason: #{output.permission_reason}"
109
+ puts "Allowed? #{output.allowed?}"
110
+ puts "Should ask? #{output.should_ask_permission?}"
111
+
112
+ # Automatic exit logic
113
+ puts "Exit code: #{output.exit_code}" # 0, 1, or 2 based on permission
114
+ puts "Stream: #{output.output_stream}" # :stdout or :stderr
115
+
116
+ # One call handles everything
117
+ hook.output_and_exit # Prints JSON to correct stream and exits with correct code
118
+ ```
119
+
120
+ ### UserPromptSubmit Hooks
121
+
122
+ ```ruby
123
+ hook = MyPromptHook.new(input_data)
124
+ hook.call
125
+ output = hook.output
126
+
127
+ # Clean semantic access
128
+ puts "Blocked? #{output.blocked?}"
129
+ puts "Reason: #{output.reason}"
130
+ puts "Context: #{output.additional_context}"
131
+
132
+ # Automatic execution
133
+ hook.output_and_exit # Handles all the exit logic
134
+ ```
135
+
136
+ ### Stop Hooks (Special Logic)
137
+
138
+ ```ruby
139
+ hook = MyStopHook.new(input_data)
140
+ hook.call
141
+ output = hook.output
142
+
143
+ # Note: Stop hooks have inverted logic - 'decision: block' means "continue working"
144
+ puts "Should continue? #{output.should_continue?}" # true if decision == 'block'
145
+ puts "Continue instructions: #{output.continue_instructions}"
146
+
147
+ hook.output_and_exit # exit 2 for "continue", exit 0 for "stop"
148
+ ```
149
+
150
+ ## Merging Multiple Hooks
151
+
152
+ ```ruby
153
+ # OLD WAY: Manual merging with class methods
154
+ result1 = hook1.call
155
+ result2 = hook2.call
156
+ merged_hash = ClaudeHooks::PreToolUse.merge_outputs(result1, result2)
157
+ # ... then manual exit logic
158
+
159
+ # NEW WAY: Clean object-based merging
160
+ hook1.call
161
+ hook2.call
162
+ merged_output = ClaudeHooks::Output::PreToolUse.merge(
163
+ hook1.output,
164
+ hook2.output
165
+ )
166
+ # /!\ Called on the merged output
167
+ merged_output.output_and_exit # Handles everything automatically
168
+ ```
169
+
170
+ ## Simplified Entrypoint Pattern
171
+
172
+ ### For Single Hooks
173
+ ```ruby
174
+ #!/usr/bin/env ruby
175
+ require 'claude_hooks'
176
+ require_relative '../handlers/my_hook'
177
+
178
+ # Super simple - just 3 lines!
179
+ ClaudeHooks::CLI.entrypoint do |input_data|
180
+ hook = MyHook.new(input_data)
181
+ hook.call
182
+ hook.output_and_exit
183
+ end
184
+ ```
185
+
186
+ ### For Multiple Hooks with Merging
187
+ ```ruby
188
+ #!/usr/bin/env ruby
189
+ require 'claude_hooks'
190
+ require_relative '../handlers/hook1'
191
+ require_relative '../handlers/hook2'
192
+
193
+ ClaudeHooks::CLI.entrypoint do |input_data|
194
+ hook1 = Hook1.new(input_data)
195
+ hook2 = Hook2.new(input_data)
196
+ hook1.call
197
+ hook2.call
198
+
199
+ merged = ClaudeHooks::Output::PreToolUse.merge(
200
+ hook1.output,
201
+ hook2.output
202
+ )
203
+ # /!\ Called on the merged output
204
+ merged.output_and_exit
205
+ end
206
+ ```
207
+
208
+ ## All Supported Hook Types
209
+
210
+ - `ClaudeHooks::Output::UserPromptSubmit`
211
+ - `ClaudeHooks::Output::PreToolUse`
212
+ - `ClaudeHooks::Output::PostToolUse`
213
+ - `ClaudeHooks::Output::Stop`
214
+ - `ClaudeHooks::Output::SubagentStop`
215
+ - `ClaudeHooks::Output::Notification`
216
+ - `ClaudeHooks::Output::SessionStart`
217
+ - `ClaudeHooks::Output::SessionEnd`
218
+ - `ClaudeHooks::Output::PreCompact`
219
+
220
+ Each handles the specific exit code logic and semantic helpers for its hook type.
221
+
222
+ ## Migration Steps
223
+
224
+ 1. **Update your hook handlers** to return ``output` instead of `output_data`
225
+ 2. **Replace manual exit logic** with `hook.output_and_exit` or `merged_output.output_and_exit`
226
+ 3. **Use semantic helpers** instead of digging through hash structures
227
+ 4. **Use output object classes for merging** instead of hook class methods
228
+
@@ -0,0 +1,83 @@
1
+ # Common API Methods
2
+
3
+ Those methods are available in **all hook types** and are inherited from `ClaudeHooks::Base`:
4
+
5
+ ## Input Helpers
6
+ Input helpers to access the data provided by Claude Code through `STDIN`.
7
+
8
+ | Method | Description |
9
+ |--------|-------------|
10
+ | `input_data` | Input data reader |
11
+ | `session_id` | Get the current session ID |
12
+ | `transcript_path` | Get path to the transcript file |
13
+ | `cwd` | Get current working directory |
14
+ | `hook_event_name` | Get the hook event name |
15
+ | `read_transcript` | Read the transcript file |
16
+ | `transcript` | Alias for `read_transcript` |
17
+
18
+ ## Hook State Helpers
19
+ Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
20
+
21
+ | Method | Description |
22
+ |--------|-------------|
23
+ | `allow_continue!` | Allow Claude to continue (default) |
24
+ | `prevent_continue!(reason)` | Stop Claude with reason |
25
+ | `suppress_output!` | Hide stdout from transcript |
26
+ | `show_output!` | Show stdout in transcript (default) |
27
+ | `clear_specifics!` | Clear hook-specific output |
28
+ | `system_message!(message)` | Set a system message shown to the user (not to Claude) |
29
+ | `clear_system_message!` | Clear the system message |
30
+
31
+ ## Output Helpers
32
+
33
+ From a hook, you can always access the `output` object via `hook.output`.
34
+ This object provides helpers to access output data, for merging multiple outputs as well as sending the right exit codes and data back to Claude Code.
35
+
36
+ ### Output data access
37
+
38
+ | Method | Description |
39
+ |--------|-------------|
40
+ | `output` | Output object accessor |
41
+ | `output_data` | RAW output data accessor |
42
+ | `output.continue?` | Check if Claude should continue processing |
43
+ | `output.stop_reason` | Get the stop reason if continue is false |
44
+ | `output.suppress_output?` | Check if output should be suppressed from transcript |
45
+ | `output.hook_specific_output` | Get the hook-specific output data |
46
+ | `output.system_message` | Get the system message if any |
47
+
48
+ ### Output data merging
49
+ For each hook type, the `output` object provides a **class method** `merge` that will try to intelligently merge multiple hook results, e.g. `ClaudeHooks::Output::UserPromptSubmit.merge(output1, output2, output3)`.
50
+
51
+ | Method | Description |
52
+ |--------|-------------|
53
+ | `merge(*outputs)` | Intelligently merge multiple outputs of the same type into a single output |
54
+
55
+ ### Exit Control and Yielding back to Claude Code
56
+
57
+ | Method | Description |
58
+ |--------|-------------|
59
+ | `output.output_and_exit` | Automatically output to correct stream and exit with proper code |
60
+ | `output.exit_code` | Get the calculated exit code based on hook state |
61
+ | `output.output_stream` | Get the proper output stream (:stdout or :stderr) depending on hook state |
62
+ | `output.to_json` | Generates a JSON string of the output |
63
+
64
+ ## Configuration and Utility Methods
65
+
66
+ ### Utility Methods
67
+ | Method | Description |
68
+ |--------|-------------|
69
+ | `log(message, level: :info)` | Log to session-specific file (levels: :info, :warn, :error) |
70
+
71
+ ### Configuration Methods
72
+ | Method | Description |
73
+ |--------|-------------|
74
+ | `home_claude_dir` | Get the home Claude directory (`$HOME/.claude`) |
75
+ | `project_claude_dir` | Get the project Claude directory (`$CLAUDE_PROJECT_DIR/.claude`, or `nil`) |
76
+ | `home_path_for(relative_path)` | Get absolute path relative to home Claude directory |
77
+ | `project_path_for(relative_path)` | Get absolute path relative to project Claude directory (or `nil`) |
78
+ | `base_dir` | Get the base Claude directory (**deprecated**) |
79
+ | `path_for(relative_path, base_dir=nil)` | Get absolute path relative to specified or default base dir (**deprecated**) |
80
+ | `config` | Access the merged configuration object |
81
+ | `config.get_config_value(env_key, config_file_key, default)` | Get any config value with fallback |
82
+ | `config.logs_directory` | Get logs directory path (always under home directory) |
83
+ | `config.your_custom_key` | Access any custom config via method_missing |
@@ -0,0 +1,32 @@
1
+ # Notification API
2
+
3
+ Available when inheriting from `ClaudeHooks::Notification`:
4
+
5
+ ## Input Helpers
6
+ Input helpers to access the data provided by Claude Code through `STDIN`.
7
+
8
+ [📚 Shared input helpers](COMMON.md#input-helpers)
9
+
10
+ | Method | Description |
11
+ |--------|-------------|
12
+ | `message` | Get the notification message content |
13
+ | `notification_message` | Alias for `message` |
14
+
15
+ ## Hook State Helpers
16
+ Notifications are outside facing and do not have any specific state to modify.
17
+
18
+ [📚 Shared hook state methods](COMMON.md#hook-state-methods)
19
+
20
+ ## Output Helpers
21
+ Output helpers provide access to the hook's output data and helper methods for working with the output state.
22
+ Notifications don't have any specific hook state and thus doesn't have any specific output helpers.
23
+
24
+ [📚 Shared output helpers](COMMON.md#output-helpers)
25
+
26
+ ## Hook Exit Codes
27
+
28
+ | Exit Code | Behavior |
29
+ |-----------|----------|
30
+ | `exit 0` | Operation continues<br/>Logged to debug only (`--debug`) |
31
+ | `exit 1` | Non-blocking error<br/>Logged to debug only (`--debug`) |
32
+ | `exit 2` | N/A<br/>Logged to debug only (`--debug`) |
@@ -0,0 +1,45 @@
1
+ # PostToolUse API
2
+
3
+ Available when inheriting from `ClaudeHooks::PostToolUse`:
4
+
5
+ ## Input Helpers
6
+ Input helpers to access the data provided by Claude Code through `STDIN`.
7
+
8
+ [📚 Shared input helpers](COMMON.md#input-helpers)
9
+
10
+ | Method | Description |
11
+ |--------|-------------|
12
+ | `tool_name` | Get the name of the tool that was used |
13
+ | `tool_input` | Get the input that was passed to the tool |
14
+ | `tool_response` | Get the tool's response/output |
15
+
16
+ ## Hook State Helpers
17
+ Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
18
+
19
+ [📚 Shared hook state methods](COMMON.md#hook-state-methods)
20
+
21
+ | Method | Description |
22
+ |--------|-------------|
23
+ | `block_tool!(reason)` | Block the tool result from being used |
24
+ | `approve_tool!(reason)` | Clear any previous block decision (default behavior) |
25
+ | `add_additional_context!(context)` | Add context for Claude to consider after tool use |
26
+
27
+ ## Output Helpers
28
+ Output helpers provide access to the hook's output data and helper methods for working with the output state.
29
+
30
+ [📚 Shared output helpers](COMMON.md#output-helpers)
31
+
32
+ | Method | Description |
33
+ |--------|-------------|
34
+ | `output.decision` | Get the decision: "block" or nil (default) |
35
+ | `output.reason` | Get the reason that was set for the decision |
36
+ | `output.blocked?` | Check if the tool result has been blocked |
37
+ | `output.additional_context` | Get the additional context that was added |
38
+
39
+ ## Hook Exit Codes
40
+
41
+ | Exit Code | Behavior |
42
+ |-----------|----------|
43
+ | `exit 0` | Operation continues<br/>`STDOUT` shown to user in transcript mode |
44
+ | `exit 1` | Non-blocking error<br/>`STDERR` shown to user |
45
+ | `exit 2` | N/A<br/>`STDERR` shown to Claude *(tool already ran)* |
@@ -0,0 +1,39 @@
1
+ # PreCompact API
2
+
3
+ Available when inheriting from `ClaudeHooks::PreCompact`:
4
+
5
+ ## Input Helpers
6
+ Input helpers to access the data provided by Claude Code through `STDIN`.
7
+
8
+ [📚 Shared input helpers](COMMON.md#input-helpers)
9
+
10
+ | Method | Description |
11
+ |--------|-------------|
12
+ | `trigger` | Get the compaction trigger: `'manual'` or `'auto'` |
13
+ | `custom_instructions` | Get custom instructions (only available for manual trigger) |
14
+
15
+ ## Hook State Helpers
16
+ No specific hook state methods are available to alter compaction behavior.
17
+
18
+ [📚 Shared hook state methods](COMMON.md#hook-state-methods)
19
+
20
+ ## Utility Methods
21
+ Utility methods for transcript management.
22
+
23
+ | Method | Description |
24
+ |--------|-------------|
25
+ | `backup_transcript!(backup_file_path)` | Create a backup of the transcript at the specified path |
26
+
27
+ ## Output Helpers
28
+ Output helpers provide access to the hook's output data and helper methods for working with the output state.
29
+ PreCompact hooks don't have any specific hook state and thus doesn't have any specific output helpers.
30
+
31
+ [📚 Shared output helpers](COMMON.md#output-helpers)
32
+
33
+ ## Hook Exit Codes
34
+
35
+ | Exit Code | Behavior |
36
+ |-----------|----------|
37
+ | `exit 0` | Operation continues<br/>`STDOUT` shown to user in transcript mode |
38
+ | `exit 1` | Non-blocking error<br/>`STDERR` shown to user |
39
+ | `exit 2` | N/A<br/>`STDERR` shown to user only|
@@ -0,0 +1,110 @@
1
+ # PreToolUse API
2
+
3
+ Available when inheriting from `ClaudeHooks::PreToolUse`:
4
+
5
+ ## Input Helpers
6
+ Input helpers to access the data provided by Claude Code through `STDIN`.
7
+
8
+ [📚 Shared input helpers](COMMON.md#input-helpers)
9
+
10
+ | Method | Description |
11
+ |--------|-------------|
12
+ | `tool_name` | Get the name of the tool being used |
13
+ | `tool_input` | Get the input data for the tool |
14
+
15
+ ## Hook State Helpers
16
+ Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
17
+
18
+ [📚 Shared hook state methods](COMMON.md#hook-state-methods)
19
+
20
+ | Method | Description |
21
+ |--------|-------------|
22
+ | `approve_tool!(reason)` | Explicitly approve tool usage |
23
+ | `block_tool!(reason)` | Block tool usage with feedback |
24
+ | `ask_for_permission!(reason)` | Request user permission |
25
+
26
+ ## Output Helpers
27
+ Output helpers provide access to the hook's output data and helper methods for working with the output state.
28
+
29
+ [📚 Shared output helpers](COMMON.md#output-helpers)
30
+
31
+ | Method | Description |
32
+ |--------|-------------|
33
+ | `output.allowed?` | Check if the tool has been explicitly allowed (permission_decision == 'allow') |
34
+ | `output.denied?` | Check if the tool has been denied (permission_decision == 'deny') |
35
+ | `output.blocked?` | Alias for `denied?` |
36
+ | `output.should_ask_permission?` | Check if user permission is required (permission_decision == 'ask') |
37
+ | `output.permission_decision` | Get the permission decision: 'allow', 'deny', or 'ask' |
38
+ | `output.permission_reason` | Get the reason for the permission decision |
39
+
40
+ ## Hook Exit Codes
41
+
42
+ | Exit Code | Behavior |
43
+ |-----------|----------|
44
+ | `exit 0` | Operation continues<br/>`STDOUT` shown to user in transcript mode |
45
+ | `exit 1` | Non-blocking error<br/>`STDERR` shown to user |
46
+ | `exit 2` | **Blocks the tool call**<br/>`STDERR` shown to Claude |
47
+
48
+ ## Exit code behaviors related to chosen output stream
49
+ Outputting to a specific stream has a different effect depending on the exit code.
50
+
51
+ > [!TIP]
52
+ > The most common and useful cases expressed in the tables below are handled automatically by calling `hook.output_and_exit`.
53
+ > You only need to worry about this when you want very specific behavior.
54
+
55
+ ### ALLOW
56
+
57
+ #### Claude Code behavior depending on combination
58
+
59
+ | Exit Code | STDERR | STDOUT |
60
+ |------------|--------|--------|
61
+ | 0 | RUNS | RUNS |
62
+ | 1 | RUNS | RUNS |
63
+ | 2 | BLOCKS | RUNS |
64
+
65
+ #### Output visibility depending on exit code
66
+
67
+ | Output Visibility / Exit Code | 0 | 1 | 2 |
68
+ |-------------------------------|-------|-------|---------|
69
+ | **STDOUT sent to Claude** | ✅ YES | ✅ YES | ✅ YES |
70
+ | **STDOUT shown to User** | ❌ NO | ❌ NO | ❌ NO |
71
+ | **STDERR sent to Claude** | ❌ NO | ❌ NO | ✅ YES |
72
+ | **STDERR shown to User** | ❌ NO | ✅ YES | ✅ YES |
73
+
74
+ ### ASK
75
+
76
+ #### Claude Code behavior depending on combination
77
+
78
+ | Exit Code | STDERR | STDOUT |
79
+ |------------|--------|--------|
80
+ | 0 | RUNS | ASKS |
81
+ | 1 | RUNS | ASKS |
82
+ | 2 | BLOCKS | ASKS |
83
+
84
+ #### Output visibility depending on exit code
85
+
86
+ | Output Visibility / Exit Code | 0 | 1 | 2 |
87
+ |-------------------------------|-------|-------|---------|
88
+ | **STDOUT sent to Claude** | ✅ YES | ✅ YES | ✅ YES |
89
+ | **STDOUT shown to User** | ✅ YES | ✅ YES | ✅ YES |
90
+ | **STDERR sent to Claude** | ❌ NO | ❌ NO | ✅ YES |
91
+ | **STDERR shown to User** | ❌ NO | ✅ YES | ✅ YES |
92
+
93
+ ### DENY
94
+
95
+ #### Claude Code behavior depending on combination
96
+
97
+ | Exit Code | STDERR | STDOUT |
98
+ |------------|--------|--------|
99
+ | 0 | BLOCKS | BLOCKS |
100
+ | 1 | BLOCKS | BLOCKS |
101
+ | 2 | BLOCKS | BLOCKS |
102
+
103
+ #### Output visibility depending on exit code
104
+
105
+ | Output Visibility / Exit Code | 0 | 1 | 2 |
106
+ |-------------------------------|-------|-------|---------|
107
+ | **STDOUT sent to Claude** | ✅ YES | ✅ YES | ✅ YES |
108
+ | **STDOUT shown to User** | ✅ YES | ✅ YES | ✅ YES |
109
+ | **STDERR sent to Claude** | ❌ NO | ❌ NO | ✅ YES |
110
+ | **STDERR shown to User** | ❌ NO | ✅ YES | ✅ YES |
@@ -0,0 +1,100 @@
1
+ # SessionEnd API
2
+
3
+ > [!NOTE]
4
+ > SessionEnd hooks cannot block session termination - they are designed for cleanup tasks only.
5
+
6
+ Available when inheriting from `ClaudeHooks::SessionEnd`:
7
+
8
+ ## Input Helpers
9
+ Input helpers to access the data provided by Claude Code through `STDIN`.
10
+
11
+ [📚 Shared input helpers](COMMON.md#input-helpers)
12
+
13
+ | Method | Description |
14
+ |--------|-------------|
15
+ | `reason` | Get the reason for session termination |
16
+
17
+ ### Reason Helpers
18
+
19
+ | Method | Description |
20
+ |--------|-------------|
21
+ | `cleared?` | Check if session was cleared with `/clear` command |
22
+ | `logout?` | Check if user logged out |
23
+ | `prompt_input_exit?` | Check if user exited while prompt input was visible |
24
+ | `other_reason?` | Check if reason is unspecified or other |
25
+
26
+ ## Hook State Helpers
27
+ Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
28
+ SessionEnd hooks do not have any specific state to modify.
29
+
30
+ [📚 Shared hook state methods](COMMON.md#hook-state-methods)
31
+
32
+ ## Output Helpers
33
+ Output helpers provide access to the hook's output data and helper methods for working with the output state.
34
+ SessionEnd hooks don't have any specific hook state and thus doesn't have any specific output helpers.
35
+
36
+ [📚 Shared output helpers](COMMON.md#output-helpers)
37
+
38
+ ## Hook Exit Codes
39
+
40
+ | Exit Code | Behavior |
41
+ |-----------|----------|
42
+ | `exit 0` | Operation continues<br/>Logged to debug only (`--debug`) |
43
+ | `exit 1` | Non-blocking error<br/>Logged to debug only (`--debug`) |
44
+ | `exit 2` | N/A<br/>Logged to debug only (`--debug`) |
45
+
46
+ ## Example Usage
47
+
48
+ ```ruby
49
+ #!/usr/bin/env ruby
50
+
51
+ require 'claude_hooks'
52
+
53
+ class MySessionCleanup < ClaudeHooks::SessionEnd
54
+ def call
55
+ log "Session ending with reason: #{reason}"
56
+
57
+ if cleared?
58
+ log "Cleaning up after /clear command"
59
+ add_cleanup_message!("Temporary files cleaned")
60
+ elsif logout?
61
+ log "User logged out - saving state"
62
+ log_session_info!("Session saved successfully")
63
+ elsif prompt_input_exit?
64
+ log "Interrupted during prompt - partial cleanup"
65
+ add_cleanup_message!("Partial state saved")
66
+ else
67
+ log "General session cleanup"
68
+ add_cleanup_message!("Session ended normally")
69
+ end
70
+
71
+ output
72
+ end
73
+ end
74
+
75
+ # CLI testing
76
+ if __FILE__ == $0
77
+ ClaudeHooks::CLI.test_runner(MySessionCleanup)
78
+ end
79
+ ```
80
+
81
+ ## Session Configuration
82
+
83
+ Register the hook in your `.claude/settings.json`:
84
+
85
+ ```json
86
+ {
87
+ "hooks": {
88
+ "SessionEnd": [
89
+ {
90
+ "hooks": [
91
+ {
92
+ "type": "command",
93
+ "command": "~/.claude/hooks/entrypoints/session_end.rb"
94
+ }
95
+ ]
96
+ }
97
+ ]
98
+ }
99
+ }
100
+ ```
@@ -0,0 +1,40 @@
1
+ # SessionStart API
2
+
3
+ Available when inheriting from `ClaudeHooks::SessionStart`:
4
+
5
+ ## Input Helpers
6
+ Input helpers to access the data provided by Claude Code through `STDIN`.
7
+
8
+ [📚 Shared input helpers](COMMON.md#input-helpers)
9
+
10
+ | Method | Description |
11
+ |--------|-------------|
12
+ | `source` | Get the session start source: `'startup'`, `'resume'`, `'clear'`, or `'compact'` |
13
+
14
+ ## Hook State Helpers
15
+ Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
16
+
17
+ [📚 Shared hook state methods](COMMON.md#hook-state-methods)
18
+
19
+ | Method | Description |
20
+ |--------|-------------|
21
+ | `add_additional_context!(context)` | Add contextual information for Claude's session |
22
+ | `add_context!(context)` | Alias for `add_additional_context!` |
23
+ | `empty_additional_context!` | Clear additional context |
24
+
25
+ ## Output Helpers
26
+ Output helpers provide access to the hook's output data and helper methods for working with the output state.
27
+
28
+ [📚 Shared output helpers](COMMON.md#output-helpers)
29
+
30
+ | Method | Description |
31
+ |--------|-------------|
32
+ | `output.additional_context` | Get the additional context that was added |
33
+
34
+ ## Hook Exit Codes
35
+
36
+ | Exit Code | Behavior |
37
+ |-----------|----------|
38
+ | `exit 0` | Operation continues<br/>**`STDOUT` added as context to Claude** |
39
+ | `exit 1` | Non-blocking error<br/>`STDERR` shown to user |
40
+ | `exit 2` | N/A<br/>`STDERR` shown to user only |