claude_hooks 1.0.0 → 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 +99 -4
- data/README.md +91 -15
- data/docs/{OUTPUT_MIGRATION_GUIDE.md → 1.0.0_MIGRATION_GUIDE.md} +17 -17
- 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/API/SESSION_START.md +1 -1
- data/docs/WHY.md +15 -8
- data/docs/external/claude-hooks-reference.md +1310 -0
- data/example_dotclaude/hooks/entrypoints/pre_tool_use.rb +25 -0
- data/example_dotclaude/hooks/handlers/pre_tool_use/github_guard.rb +253 -0
- data/example_dotclaude/hooks/handlers/user_prompt_submit/append_rules.rb +2 -1
- data/example_dotclaude/plugins/README.md +175 -0
- data/example_dotclaude/settings.json +11 -0
- 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/post_tool_use.rb +14 -6
- data/lib/claude_hooks/output/pre_tool_use.rb +47 -32
- data/lib/claude_hooks/output/stop.rb +13 -6
- data/lib/claude_hooks/output/user_prompt_submit.rb +14 -7
- 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/stop.rb +8 -0
- data/lib/claude_hooks/version.rb +1 -1
- data/lib/claude_hooks.rb +2 -0
- metadata +12 -3
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Upgrading to v1.1.0
|
|
2
|
+
|
|
3
|
+
Version 1.1.0 adds new features to align with Anthropic's updated Claude Code Hooks documentation. **All changes are backward compatible** - existing hooks continue to work without modification.
|
|
4
|
+
|
|
5
|
+
## What's New
|
|
6
|
+
|
|
7
|
+
### 1. New PermissionRequest Hook
|
|
8
|
+
|
|
9
|
+
Handle permission request events from Claude Code:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class PermissionGuard < ClaudeHooks::PermissionRequest
|
|
13
|
+
def call
|
|
14
|
+
if dangerous_tool?
|
|
15
|
+
deny_permission!("Requires manual approval")
|
|
16
|
+
else
|
|
17
|
+
allow_permission!("Safe to proceed")
|
|
18
|
+
end
|
|
19
|
+
output
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def dangerous_tool?
|
|
25
|
+
%w[rm git-reset curl].include?(tool_name)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**When to use**: Create permission guards that automatically approve/deny permission requests based on custom logic.
|
|
31
|
+
|
|
32
|
+
### 2. New Fields Available
|
|
33
|
+
|
|
34
|
+
All hooks now receive these additional fields:
|
|
35
|
+
|
|
36
|
+
- **`permission_mode`** (all hooks): Access via `permission_mode` method
|
|
37
|
+
- Values: `'default'`, `'plan'`, `'acceptEdits'`, `'dontAsk'`, `'bypassPermissions'`
|
|
38
|
+
- Defaults to `'default'`
|
|
39
|
+
|
|
40
|
+
- **`tool_use_id`** (PreToolUse, PostToolUse, PermissionRequest): Access via `tool_use_id` method
|
|
41
|
+
- Unique identifier for each tool use (e.g., `"toolu_01ABC123..."`)
|
|
42
|
+
|
|
43
|
+
- **`notification_type`** (Notification): Access via `notification_type` method
|
|
44
|
+
- Values: `'permission_prompt'`, `'idle_prompt'`, `'auth_success'`, `'elicitation_dialog'`
|
|
45
|
+
|
|
46
|
+
**Example usage:**
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
class MyHook < ClaudeHooks::PreToolUse
|
|
50
|
+
def call
|
|
51
|
+
log "Permission mode: #{permission_mode}"
|
|
52
|
+
log "Tool use ID: #{tool_use_id}"
|
|
53
|
+
|
|
54
|
+
# Adjust behavior based on permission mode
|
|
55
|
+
if permission_mode == 'bypassPermissions'
|
|
56
|
+
# More restrictive checks
|
|
57
|
+
deny_permission!("Cannot bypass for sensitive operations")
|
|
58
|
+
else
|
|
59
|
+
approve_tool!("Approved")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
output
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Update Tool Input (PreToolUse)
|
|
68
|
+
|
|
69
|
+
Modify tool input before execution:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
class InputModifier < ClaudeHooks::PreToolUse
|
|
73
|
+
def call
|
|
74
|
+
case tool_name
|
|
75
|
+
when 'Bash'
|
|
76
|
+
# Add safety flags to bash commands
|
|
77
|
+
if tool_input['command']&.include?('rm')
|
|
78
|
+
sanitized = tool_input.merge('command' => "#{tool_input['command']} --interactive")
|
|
79
|
+
update_tool_input!(sanitized)
|
|
80
|
+
else
|
|
81
|
+
approve_tool!("Safe command")
|
|
82
|
+
end
|
|
83
|
+
when 'Write'
|
|
84
|
+
# Validate file paths
|
|
85
|
+
if tool_input['file_path']&.start_with?('/etc')
|
|
86
|
+
deny_permission!("Cannot write to /etc")
|
|
87
|
+
else
|
|
88
|
+
approve_tool!("Safe write")
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
approve_tool!("No special handling")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
output
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Output helpers:**
|
|
100
|
+
- `output.input_updated?` - Check if input was modified
|
|
101
|
+
- `output.updated_input` - Get the updated input hash
|
|
102
|
+
|
|
103
|
+
## Do I Need to Change My Existing Hooks?
|
|
104
|
+
|
|
105
|
+
**No!** All changes are backward compatible:
|
|
106
|
+
|
|
107
|
+
- New fields have sensible defaults (return `nil` or `'default'`)
|
|
108
|
+
- Existing methods continue to work exactly as before
|
|
109
|
+
- No breaking changes to any APIs
|
|
110
|
+
- Your existing hooks will work with v1.1.0 without any modifications
|
|
111
|
+
|
|
112
|
+
## Should I Update My Hooks?
|
|
113
|
+
|
|
114
|
+
Consider updating if:
|
|
115
|
+
|
|
116
|
+
- ✅ You want to handle PermissionRequest events
|
|
117
|
+
- ✅ You need to check the current permission mode
|
|
118
|
+
- ✅ You want to modify tool inputs dynamically
|
|
119
|
+
- ✅ You need to differentiate notification types
|
|
120
|
+
- ✅ You want to track specific tool use instances via IDs
|
|
121
|
+
|
|
122
|
+
## New Hook Type Summary
|
|
123
|
+
|
|
124
|
+
| Hook Type | When It Runs | Key Methods |
|
|
125
|
+
|-----------|--------------|-------------|
|
|
126
|
+
| **PermissionRequest** | When Claude requests permission | `allow_permission!`, `deny_permission!`, `update_input_and_allow!` |
|
|
127
|
+
|
|
128
|
+
## New Fields Summary
|
|
129
|
+
|
|
130
|
+
| Field | Available In | Purpose |
|
|
131
|
+
|-------|--------------|---------|
|
|
132
|
+
| `permission_mode` | All hooks | Know the current permission mode |
|
|
133
|
+
| `tool_use_id` | PreToolUse, PermissionRequest, PostToolUse | Track specific tool use instances |
|
|
134
|
+
| `notification_type` | Notification | Filter notifications by type |
|
|
135
|
+
|
|
136
|
+
## New PreToolUse Features
|
|
137
|
+
|
|
138
|
+
| Feature | Method | Description |
|
|
139
|
+
|---------|--------|-------------|
|
|
140
|
+
| Update input | `update_tool_input!(hash)` | Modify tool input and auto-approve |
|
|
141
|
+
| Check if updated | `output.input_updated?` | Boolean check for modification |
|
|
142
|
+
| Get updated input | `output.updated_input` | Retrieve the modified input |
|
|
143
|
+
|
|
144
|
+
## Testing
|
|
145
|
+
|
|
146
|
+
All existing tests should pass without modification. Run:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
bundle exec rake test
|
|
150
|
+
# or
|
|
151
|
+
ruby test/run_all_tests.rb
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Migration Examples
|
|
155
|
+
|
|
156
|
+
### Example 1: Using permission_mode
|
|
157
|
+
|
|
158
|
+
**Before (v1.0.x):**
|
|
159
|
+
```ruby
|
|
160
|
+
class GitGuard < ClaudeHooks::PreToolUse
|
|
161
|
+
def call
|
|
162
|
+
if tool_name == 'Bash' && tool_input['command']&.include?('git push')
|
|
163
|
+
ask_for_permission!("Git push requires confirmation")
|
|
164
|
+
else
|
|
165
|
+
approve_tool!("Approved")
|
|
166
|
+
end
|
|
167
|
+
output
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**After (v1.1.0 - enhanced):**
|
|
173
|
+
```ruby
|
|
174
|
+
class GitGuard < ClaudeHooks::PreToolUse
|
|
175
|
+
def call
|
|
176
|
+
if tool_name == 'Bash' && tool_input['command']&.include?('git push')
|
|
177
|
+
# Skip asking in dontAsk mode, but still log
|
|
178
|
+
if permission_mode == 'dontAsk'
|
|
179
|
+
log "Auto-approving git push in dontAsk mode", level: :warn
|
|
180
|
+
approve_tool!("Auto-approved due to permission mode")
|
|
181
|
+
else
|
|
182
|
+
ask_for_permission!("Git push requires confirmation")
|
|
183
|
+
end
|
|
184
|
+
else
|
|
185
|
+
approve_tool!("Approved")
|
|
186
|
+
end
|
|
187
|
+
output
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Example 2: Using PermissionRequest
|
|
193
|
+
|
|
194
|
+
**Before (v1.0.x):**
|
|
195
|
+
```ruby
|
|
196
|
+
# Permission requests couldn't be handled automatically
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**After (v1.1.0 - new capability):**
|
|
200
|
+
```ruby
|
|
201
|
+
class AutoPermissionGuard < ClaudeHooks::PermissionRequest
|
|
202
|
+
SAFE_TOOLS = %w[ls cat grep find read].freeze
|
|
203
|
+
|
|
204
|
+
def call
|
|
205
|
+
if SAFE_TOOLS.include?(tool_name)
|
|
206
|
+
allow_permission!("Safe tool, auto-approved")
|
|
207
|
+
else
|
|
208
|
+
# Let user decide
|
|
209
|
+
deny_permission!("Requires manual approval")
|
|
210
|
+
end
|
|
211
|
+
output
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Example 3: Updating tool input
|
|
217
|
+
|
|
218
|
+
**Before (v1.0.x):**
|
|
219
|
+
```ruby
|
|
220
|
+
class BashSanitizer < ClaudeHooks::PreToolUse
|
|
221
|
+
def call
|
|
222
|
+
# Could only approve or deny, not modify
|
|
223
|
+
if tool_input['command']&.include?('rm')
|
|
224
|
+
block_tool!("Dangerous command blocked")
|
|
225
|
+
else
|
|
226
|
+
approve_tool!("Safe")
|
|
227
|
+
end
|
|
228
|
+
output
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**After (v1.1.0 - new capability):**
|
|
234
|
+
```ruby
|
|
235
|
+
class BashSanitizer < ClaudeHooks::PreToolUse
|
|
236
|
+
def call
|
|
237
|
+
if tool_input['command']&.include?('rm')
|
|
238
|
+
# Add safety flags instead of blocking
|
|
239
|
+
safe_command = "#{tool_input['command']} --interactive --verbose"
|
|
240
|
+
update_tool_input!({'command' => safe_command})
|
|
241
|
+
log "Added safety flags to rm command"
|
|
242
|
+
else
|
|
243
|
+
approve_tool!("Safe")
|
|
244
|
+
end
|
|
245
|
+
output
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Questions?
|
|
251
|
+
|
|
252
|
+
- See the [full CHANGELOG](../CHANGELOG.md) for all changes
|
|
253
|
+
- Check the [API documentation](API/) for detailed method references
|
|
254
|
+
- Review the [PermissionRequest API](API/PERMISSION_REQUEST.md) for the new hook type
|
|
255
|
+
- Look at [example implementations](../example_dotclaude/) for patterns
|
|
256
|
+
|
|
257
|
+
## Rollback
|
|
258
|
+
|
|
259
|
+
If you need to rollback to v1.0.2:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
gem uninstall claude_hooks
|
|
263
|
+
gem install claude_hooks -v 1.0.2
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Or in your Gemfile:
|
|
267
|
+
```ruby
|
|
268
|
+
gem 'claude_hooks', '~> 1.0.2'
|
|
269
|
+
```
|
data/docs/API/COMMON.md
CHANGED
|
@@ -12,6 +12,7 @@ Input helpers to access the data provided by Claude Code through `STDIN`.
|
|
|
12
12
|
| `transcript_path` | Get path to the transcript file |
|
|
13
13
|
| `cwd` | Get current working directory |
|
|
14
14
|
| `hook_event_name` | Get the hook event name |
|
|
15
|
+
| `permission_mode` | Get the permission mode: `'default'`, `'plan'`, `'acceptEdits'`, `'dontAsk'`, `'bypassPermissions'` (defaults to `'default'`) |
|
|
15
16
|
| `read_transcript` | Read the transcript file |
|
|
16
17
|
| `transcript` | Alias for `read_transcript` |
|
|
17
18
|
|
data/docs/API/NOTIFICATION.md
CHANGED
|
@@ -11,6 +11,7 @@ Input helpers to access the data provided by Claude Code through `STDIN`.
|
|
|
11
11
|
|--------|-------------|
|
|
12
12
|
| `message` | Get the notification message content |
|
|
13
13
|
| `notification_message` | Alias for `message` |
|
|
14
|
+
| `notification_type` | Get the notification type: `'permission_prompt'`, `'idle_prompt'`, `'auth_success'`, `'elicitation_dialog'` |
|
|
14
15
|
|
|
15
16
|
## Hook State Helpers
|
|
16
17
|
Notifications are outside facing and do not have any specific state to modify.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# PermissionRequest API
|
|
2
|
+
|
|
3
|
+
Available when inheriting from `ClaudeHooks::PermissionRequest`:
|
|
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 requiring permission |
|
|
13
|
+
| `tool_input` | Get the input data for the tool |
|
|
14
|
+
| `tool_use_id` | Get the unique identifier for this tool use |
|
|
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
|
+
| `allow_permission!(reason = '')` | Allow the permission request with optional reason |
|
|
24
|
+
| `deny_permission!(reason = '')` | Deny the permission request with reason |
|
|
25
|
+
| `update_input_and_allow!(updated_input, reason = '')` | Update tool input and allow (convenience method) |
|
|
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.allowed?` | Check if permission has been allowed |
|
|
35
|
+
| `output.denied?` | Check if permission has been denied |
|
|
36
|
+
| `output.input_updated?` | Check if tool input has been updated |
|
|
37
|
+
| `output.permission_decision` | Get the permission decision: 'allow' or 'deny' |
|
|
38
|
+
| `output.permission_reason` | Get the reason for the permission decision |
|
|
39
|
+
| `output.updated_input` | Get the updated input (if provided) |
|
|
40
|
+
|
|
41
|
+
## Hook Exit Codes
|
|
42
|
+
|
|
43
|
+
PermissionRequest hooks use the JSON API with exit code 0 for all permission decisions.
|
|
44
|
+
|
|
45
|
+
| Exit Code | Behavior |
|
|
46
|
+
|-----------|----------|
|
|
47
|
+
| `exit 0` | Permission decision processed<br/>`STDOUT` contains JSON with decision |
|
|
48
|
+
| `exit 1` | Non-blocking error<br/>`STDERR` shown to user |
|
|
49
|
+
| `exit 2` | **Not recommended for PermissionRequest**<br/>Use JSON API with exit 0 instead |
|
|
50
|
+
|
|
51
|
+
## Example: Basic Permission Guard
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
#!/usr/bin/env ruby
|
|
55
|
+
require 'claude_hooks'
|
|
56
|
+
|
|
57
|
+
class PermissionGuard < ClaudeHooks::PermissionRequest
|
|
58
|
+
SENSITIVE_TOOLS = %w[rm git-push curl wget].freeze
|
|
59
|
+
SAFE_TOOLS = %w[ls cat grep find].freeze
|
|
60
|
+
|
|
61
|
+
def call
|
|
62
|
+
log "Permission requested for: #{tool_name}"
|
|
63
|
+
log "Tool use ID: #{tool_use_id}"
|
|
64
|
+
log "Permission mode: #{permission_mode}"
|
|
65
|
+
|
|
66
|
+
# Auto-allow safe tools
|
|
67
|
+
if SAFE_TOOLS.include?(tool_name)
|
|
68
|
+
allow_permission!("Safe tool, automatically allowed")
|
|
69
|
+
return output
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Block dangerous tools
|
|
73
|
+
if SENSITIVE_TOOLS.include?(tool_name)
|
|
74
|
+
deny_permission!("Dangerous tool #{tool_name} requires manual approval")
|
|
75
|
+
return output
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Default: allow with logging
|
|
79
|
+
allow_permission!("Tool allowed by default")
|
|
80
|
+
output
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if __FILE__ == $PROGRAM_NAME
|
|
85
|
+
input_data = JSON.parse(STDIN.read)
|
|
86
|
+
hook = PermissionGuard.new(input_data)
|
|
87
|
+
hook.call
|
|
88
|
+
hook.output_and_exit
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Example: Permission Mode-Aware Guard
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
#!/usr/bin/env ruby
|
|
96
|
+
require 'claude_hooks'
|
|
97
|
+
|
|
98
|
+
class SmartPermissionGuard < ClaudeHooks::PermissionRequest
|
|
99
|
+
DANGEROUS_TOOLS = %w[rm git-reset curl wget].freeze
|
|
100
|
+
|
|
101
|
+
def call
|
|
102
|
+
log "Permission mode: #{permission_mode}"
|
|
103
|
+
|
|
104
|
+
# Block dangerous tools in bypass mode
|
|
105
|
+
if permission_mode == 'bypassPermissions' && dangerous_tool?
|
|
106
|
+
log "Blocking dangerous tool in bypass mode", level: :warn
|
|
107
|
+
deny_permission!("Cannot bypass permissions for dangerous tool: #{tool_name}")
|
|
108
|
+
return output
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Allow in 'dontAsk' mode if tool is not dangerous
|
|
112
|
+
if permission_mode == 'dontAsk' && !dangerous_tool?
|
|
113
|
+
allow_permission!("Auto-allowed in dontAsk mode")
|
|
114
|
+
return output
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Default behavior
|
|
118
|
+
allow_permission!("Permission granted")
|
|
119
|
+
output
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def dangerous_tool?
|
|
125
|
+
DANGEROUS_TOOLS.include?(tool_name)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Example: Input Modification
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
#!/usr/bin/env ruby
|
|
134
|
+
require 'claude_hooks'
|
|
135
|
+
|
|
136
|
+
class InputSanitizer < ClaudeHooks::PermissionRequest
|
|
137
|
+
def call
|
|
138
|
+
case tool_name
|
|
139
|
+
when 'Bash'
|
|
140
|
+
# Add safety flags to bash commands
|
|
141
|
+
if tool_input['command']&.include?('rm')
|
|
142
|
+
sanitized_input = tool_input.merge(
|
|
143
|
+
'command' => tool_input['command'] + ' --interactive'
|
|
144
|
+
)
|
|
145
|
+
update_input_and_allow!(sanitized_input, "Added --interactive flag for safety")
|
|
146
|
+
else
|
|
147
|
+
allow_permission!("Bash command is safe")
|
|
148
|
+
end
|
|
149
|
+
when 'Write'
|
|
150
|
+
# Validate file paths before writing
|
|
151
|
+
if tool_input['file_path']&.start_with?('/tmp')
|
|
152
|
+
deny_permission!("Cannot write to /tmp directory")
|
|
153
|
+
else
|
|
154
|
+
allow_permission!("File write is safe")
|
|
155
|
+
end
|
|
156
|
+
else
|
|
157
|
+
allow_permission!("No special handling needed")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
output
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Multiple Hooks Merging
|
|
166
|
+
|
|
167
|
+
When multiple PermissionRequest hooks are executed, their outputs merge with these rules:
|
|
168
|
+
|
|
169
|
+
- **Decision**: `deny` > `allow` (most restrictive wins)
|
|
170
|
+
- **Reasons**: All reasons are concatenated with `'; '`
|
|
171
|
+
- **Updated Input**: Last updated input wins (most recent transformation)
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# Hook 1
|
|
175
|
+
hook1.deny_permission!("Reason 1")
|
|
176
|
+
|
|
177
|
+
# Hook 2
|
|
178
|
+
hook2.allow_permission!("Reason 2")
|
|
179
|
+
|
|
180
|
+
# Merged result
|
|
181
|
+
merged = ClaudeHooks::Output::PermissionRequest.merge(
|
|
182
|
+
hook1.output,
|
|
183
|
+
hook2.output
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
merged.denied? # => true (deny wins)
|
|
187
|
+
merged.permission_reason # => "Reason 1; Reason 2"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Notes
|
|
191
|
+
|
|
192
|
+
- PermissionRequest runs when Claude Code shows a permission dialog
|
|
193
|
+
- It can automatically allow or deny on behalf of the user
|
|
194
|
+
- Uses the same JSON API pattern as PreToolUse hooks
|
|
195
|
+
- Supports modifying tool inputs before execution
|
|
196
|
+
- Works with all permission modes: `default`, `plan`, `acceptEdits`, `dontAsk`, `bypassPermissions`
|
data/docs/API/POST_TOOL_USE.md
CHANGED
|
@@ -12,6 +12,7 @@ Input helpers to access the data provided by Claude Code through `STDIN`.
|
|
|
12
12
|
| `tool_name` | Get the name of the tool that was used |
|
|
13
13
|
| `tool_input` | Get the input that was passed to the tool |
|
|
14
14
|
| `tool_response` | Get the tool's response/output |
|
|
15
|
+
| `tool_use_id` | Get the unique identifier for this tool use (e.g., `"toolu_01ABC123..."`) |
|
|
15
16
|
|
|
16
17
|
## Hook State Helpers
|
|
17
18
|
Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
|
data/docs/API/PRE_TOOL_USE.md
CHANGED
|
@@ -11,6 +11,7 @@ Input helpers to access the data provided by Claude Code through `STDIN`.
|
|
|
11
11
|
|--------|-------------|
|
|
12
12
|
| `tool_name` | Get the name of the tool being used |
|
|
13
13
|
| `tool_input` | Get the input data for the tool |
|
|
14
|
+
| `tool_use_id` | Get the unique identifier for this tool use (e.g., `"toolu_01ABC123..."`) |
|
|
14
15
|
|
|
15
16
|
## Hook State Helpers
|
|
16
17
|
Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
|
|
@@ -22,6 +23,7 @@ Hook state methods are helpers to modify the hook's internal state (`output_data
|
|
|
22
23
|
| `approve_tool!(reason)` | Explicitly approve tool usage |
|
|
23
24
|
| `block_tool!(reason)` | Block tool usage with feedback |
|
|
24
25
|
| `ask_for_permission!(reason)` | Request user permission |
|
|
26
|
+
| `update_tool_input!(updated_input)` | Update the tool input and automatically approve the tool (sets `permissionDecision` to `'allow'`) |
|
|
25
27
|
|
|
26
28
|
## Output Helpers
|
|
27
29
|
Output helpers provide access to the hook's output data and helper methods for working with the output state.
|
|
@@ -34,8 +36,10 @@ Output helpers provide access to the hook's output data and helper methods for w
|
|
|
34
36
|
| `output.denied?` | Check if the tool has been denied (permission_decision == 'deny') |
|
|
35
37
|
| `output.blocked?` | Alias for `denied?` |
|
|
36
38
|
| `output.should_ask_permission?` | Check if user permission is required (permission_decision == 'ask') |
|
|
39
|
+
| `output.input_updated?` | Check if tool input has been updated |
|
|
37
40
|
| `output.permission_decision` | Get the permission decision: 'allow', 'deny', or 'ask' |
|
|
38
41
|
| `output.permission_reason` | Get the reason for the permission decision |
|
|
42
|
+
| `output.updated_input` | Get the updated input (if provided) |
|
|
39
43
|
|
|
40
44
|
## Hook Exit Codes
|
|
41
45
|
|
data/docs/API/SESSION_START.md
CHANGED
|
@@ -9,7 +9,7 @@ Input helpers to access the data provided by Claude Code through `STDIN`.
|
|
|
9
9
|
|
|
10
10
|
| Method | Description |
|
|
11
11
|
|--------|-------------|
|
|
12
|
-
| `source` | Get the session start source: `'startup'`, `'resume'`, or `'
|
|
12
|
+
| `source` | Get the session start source: `'startup'`, `'resume'`, `'clear'`, or `'compact'` |
|
|
13
13
|
|
|
14
14
|
## Hook State Helpers
|
|
15
15
|
Hook state methods are helpers to modify the hook's internal state (`output_data`) before yielding back to Claude Code.
|
data/docs/WHY.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
When creating fairly complex hook systems for Claude Code it is easy to end up either with:
|
|
4
4
|
- Fairly monolithic scripts that becomes hard to maintain and have to handle complex merging logic for the output.
|
|
5
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
|
+
- Having to understand the very chaotic exit code and output steam logic of Claude Code and how to handle it.
|
|
6
7
|
|
|
7
8
|
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
|
|
|
@@ -25,9 +26,9 @@ settings.json
|
|
|
25
26
|
|
|
26
27
|
## For both approaches it will bring
|
|
27
28
|
|
|
28
|
-
1. Normalized Input/Output handling - input parsing, validation, straightforward output helpers (block_tool
|
|
29
|
+
1. Normalized Input/Output handling - input parsing, validation, straightforward output helpers (`ask_for_permission!`, `approve_tool!`, `block_tool!`, `block_prompt!`, `add_context!`).
|
|
29
30
|
|
|
30
|
-
2. Hook-Specific APIs -
|
|
31
|
+
2. Hook-Specific APIs - all 9 hook types with tailored methods and output objects (with `output_and_exit`) and smart merge logic for combining outputs.
|
|
31
32
|
|
|
32
33
|
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
|
|
|
@@ -35,7 +36,6 @@ settings.json
|
|
|
35
36
|
|
|
36
37
|
5. Testing Support - Standalone execution mode for individual hook testing and CLI testing with sample JSON input.
|
|
37
38
|
|
|
38
|
-
|
|
39
39
|
## For a monolithic approach it will additionally bring
|
|
40
40
|
|
|
41
41
|
1. Composable Hook Handlers
|
|
@@ -43,11 +43,19 @@ For instance, `entrypoints/user_prompt_submit.rb` orchestrates multiple handlers
|
|
|
43
43
|
```ruby
|
|
44
44
|
# Add contextual rules
|
|
45
45
|
append_rules_result = AppendRules.new(input_data).call
|
|
46
|
+
append_rules_result.call
|
|
47
|
+
|
|
46
48
|
# Audit logging
|
|
47
49
|
log_result = LogUserPrompt.new(input_data).call
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
log_result.call
|
|
51
|
+
|
|
52
|
+
# Merge outputs and yield back to Claude Code
|
|
53
|
+
merged = ClaudeHooks::Output::UserPromptSubmit.merge(
|
|
54
|
+
append_rules.output,
|
|
55
|
+
log_handler.output
|
|
56
|
+
)
|
|
57
|
+
merged.output_and_exit
|
|
58
|
+
end
|
|
51
59
|
```
|
|
52
60
|
|
|
53
61
|
2. Intelligent Output Merging
|
|
@@ -59,7 +67,6 @@ puts ClaudeHooks::UserPromptSubmit.merge_outputs(append_rules_result, log_result
|
|
|
59
67
|
Each handler can run standalone for testing:
|
|
60
68
|
```ruby
|
|
61
69
|
if __FILE__ == $0
|
|
62
|
-
|
|
63
|
-
puts hook.stringify_output
|
|
70
|
+
ClaudeHooks::CLI.test_runner(AppendRules)
|
|
64
71
|
end
|
|
65
72
|
```
|