claude_hooks 1.2.0 → 1.2.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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +149 -220
- data/docs/1.0.0_MIGRATION_GUIDE.md +6 -7
- data/example_dotclaude/hooks/entrypoints/session_end.rb +5 -22
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit.rb +4 -26
- data/example_dotclaude/hooks/{handlers/pre_tool_use/github_guard.rb → github_guard.rb} +7 -13
- data/example_dotclaude/settings.json +1 -1
- data/lib/claude_hooks/cli.rb +84 -149
- data/lib/claude_hooks/version.rb +1 -1
- metadata +2 -4
- data/.claude/settings.local.json +0 -21
- data/example_dotclaude/hooks/entrypoints/pre_tool_use.rb +0 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ff845df334960263a1d36b01aeba57393609c59acbc63424572c9d7fdca7999
|
|
4
|
+
data.tar.gz: 70029c193102d9a63b6d6e5ff8361fcf42e506bff26435c907ec4a06010a571a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28a4548cafd93c7f5d5c9d1f577399d8e90503c2e4ae1fa279a9723234045848c0b3bdbb1c41df9f0808d375eb5d345a5210d9db46fe74ba3332fb2394920530
|
|
7
|
+
data.tar.gz: e2555f3731aad5bacb7e572c9630fa9e39721b625684664bc2ea41d9b0579af146f697b10a6d99410368140ebb7cd37de53b2bd195e2a2c13a5100722502fcd2
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.1] - 2026-07-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`CLI.run_hook` — new primary entrypoint method** replacing `CLI.entrypoint`. Reads JSON from STDIN, runs the hook, and exits with the correct code and stream for the hook type.
|
|
13
|
+
- Accepts `on_error: :allow` (default, exit 1 — non-blocking) or `on_error: :block` (exit 2 — blocking). Use `:block` for security/policy hooks where a crash should never silently pass through.
|
|
14
|
+
- Supports a class form (`CLI.run_hook(MyHook)`) and a block form for multi-handler merging.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **`CLI.entrypoint` deprecated** in favour of `CLI.run_hook`. It remains as a delegating alias with a deprecation warning and will be removed in a future minor version.
|
|
19
|
+
- **`CLI.run_hook` (old)** — the previous public `run_hook(hook_class, input_data)` helper used internally by `test_runner`/`run_with_sample_data` has been renamed to `run_hook_with_data` and made private. It was never intended as part of the public API.
|
|
20
|
+
- **Error output stream** — `CLI.run_hook` / `CLI.entrypoint` now write errors exclusively to stderr (previously the `:allow` path also wrote to stdout). On the `on_error: :block` (exit 2) path the message is written as **plain text**, not JSON, since Claude Code shows exit-2 stderr to the model verbatim and never parses it as JSON. The `:allow` (exit 1) path still writes the JSON error object to stderr.
|
|
21
|
+
- **README and examples** updated throughout to use `CLI.run_hook`.
|
|
22
|
+
|
|
23
|
+
### Notes
|
|
24
|
+
|
|
25
|
+
- No hook class or output class changes — existing hook scripts continue to work without modification
|
|
26
|
+
- `CLI.entrypoint` still works; only a deprecation warning is emitted
|
|
27
|
+
- 3 new tests covering `on_error: :block` (hook error → exit 2, invalid JSON → exit 2) and the deprecation warning
|
|
28
|
+
|
|
8
29
|
## [1.2.0] - 2026-07-14
|
|
9
30
|
|
|
10
31
|
### Added
|
data/README.md
CHANGED
|
@@ -50,10 +50,8 @@ Here's how to create a simple command hook with this DSL:
|
|
|
50
50
|
1. **Create a simple hook script**
|
|
51
51
|
```ruby
|
|
52
52
|
#!/usr/bin/env ruby
|
|
53
|
-
require 'json'
|
|
54
53
|
require 'claude_hooks'
|
|
55
54
|
|
|
56
|
-
# Inherit from the right hook type class to get access to helper methods
|
|
57
55
|
class AddContextAfterPrompt < ClaudeHooks::UserPromptSubmit
|
|
58
56
|
def call
|
|
59
57
|
log "User asked: #{prompt}"
|
|
@@ -62,18 +60,7 @@ class AddContextAfterPrompt < ClaudeHooks::UserPromptSubmit
|
|
|
62
60
|
end
|
|
63
61
|
end
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
if __FILE__ == $0
|
|
67
|
-
# Read Claude Code's input data from STDIN
|
|
68
|
-
input_data = JSON.parse(STDIN.read)
|
|
69
|
-
|
|
70
|
-
hook = AddContextAfterPrompt.new(input_data)
|
|
71
|
-
hook.call
|
|
72
|
-
|
|
73
|
-
# Handles output and exit code depending on the hook state.
|
|
74
|
-
# In this case, uses exit code 0 (success) and prints output to STDOUT
|
|
75
|
-
hook.output_and_exit
|
|
76
|
-
end
|
|
63
|
+
ClaudeHooks::CLI.run_hook(AddContextAfterPrompt)
|
|
77
64
|
```
|
|
78
65
|
|
|
79
66
|
3. ⚠️ **Make it executable**
|
|
@@ -102,7 +89,7 @@ echo '{"session_id":"test","prompt":"Hello!"}' | ./add_context_after_prompt.rb
|
|
|
102
89
|
That's it! Your hook will now add context to every user prompt. 🎉
|
|
103
90
|
|
|
104
91
|
> [!TIP]
|
|
105
|
-
>
|
|
92
|
+
> Need to run multiple hooks for the same event and merge their outputs? See [Multi-hook structure](#multi-hook-structure) below.
|
|
106
93
|
|
|
107
94
|
## 📦 Installation
|
|
108
95
|
|
|
@@ -232,34 +219,47 @@ end
|
|
|
232
219
|
### Core Components
|
|
233
220
|
|
|
234
221
|
1. **`ClaudeHooks::Base`** - Base class with common functionality (logging, config, validation)
|
|
235
|
-
2. **Hook
|
|
236
|
-
3. **Output Classes
|
|
237
|
-
4.
|
|
238
|
-
5. **
|
|
222
|
+
2. **Hook Classes** - One class per event type (`ClaudeHooks::UserPromptSubmit`, `ClaudeHooks::PreToolUse`, etc.) that you can inherit from in your hook scripts
|
|
223
|
+
3. **Output Classes**: those hook classes return instances of output objects (`ClaudeHooks::Output::UserPromptSubmit`, etc.) that handle intelligent merging of multiple outputs, correct exit codes, and routing to `STDOUT` or `STDERR` depending on hook state
|
|
224
|
+
4. **`ClaudeHooks::CLI`** - Entrypoint helpers: `CLI.run_hook` for production, `CLI.test_runner`/`CLI.run_with_sample_data` for local testing
|
|
225
|
+
5. **Configuration** - Shared configuration management via `ClaudeHooks::Configuration`
|
|
226
|
+
6. **Logger** - Dedicated logging class with multiline block support
|
|
239
227
|
|
|
240
|
-
###
|
|
228
|
+
### Hook file structure
|
|
229
|
+
|
|
230
|
+
For simple cases like one hook class per event, a single file is all you need. Name each file after what it does — the event it runs on comes from where you register it in `settings.json`, not from the filename:
|
|
241
231
|
|
|
242
232
|
```
|
|
243
233
|
.claude/hooks/
|
|
244
|
-
├──
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
│ ├── pre_compact.rb
|
|
249
|
-
│ ├── session_start.rb
|
|
250
|
-
│ ├── stop.rb
|
|
251
|
-
│ └── subagent_stop.rb
|
|
252
|
-
|
|
|
253
|
-
└── handlers/ # Hook handlers for specific hook type
|
|
254
|
-
├── user_prompt_submit/
|
|
255
|
-
│ ├── append_rules.rb
|
|
256
|
-
│ └── log_user_prompt.rb
|
|
257
|
-
├── pre_tool_use/
|
|
258
|
-
│ ├── github_guard.rb
|
|
259
|
-
│ └── tool_monitor.rb
|
|
260
|
-
└── ...
|
|
234
|
+
├── github_guard.rb # PreToolUse — ClaudeHooks::CLI.run_hook(GithubGuard, on_error: :block)
|
|
235
|
+
├── format_on_write.rb # PostToolUse — ClaudeHooks::CLI.run_hook(FormatOnWrite)
|
|
236
|
+
├── load_project_rules.rb # SessionStart — ClaudeHooks::CLI.run_hook(LoadProjectRules)
|
|
237
|
+
└── append_rules.rb # UserPromptSubmit — ClaudeHooks::CLI.run_hook(AppendRules)
|
|
261
238
|
```
|
|
262
239
|
|
|
240
|
+
See [`example_dotclaude/hooks/github_guard.rb`](example_dotclaude/hooks/github_guard.rb) for a complete, self-contained `PreToolUse` hook wired up this way.
|
|
241
|
+
|
|
242
|
+
### Multi-hook structure
|
|
243
|
+
|
|
244
|
+
When you need to run multiple hooks for the same event and merge their outputs, split into entrypoints and handlers:
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
.claude/hooks/
|
|
248
|
+
├── entrypoints/ # Coordinates multiple handlers per event
|
|
249
|
+
│ ├── session_end.rb
|
|
250
|
+
│ └── user_prompt_submit.rb
|
|
251
|
+
│
|
|
252
|
+
└── handlers/ # One class per concern
|
|
253
|
+
├── session_end/
|
|
254
|
+
│ ├── cleanup_handler.rb
|
|
255
|
+
│ └── log_session_stats.rb
|
|
256
|
+
└── user_prompt_submit/
|
|
257
|
+
├── append_rules.rb
|
|
258
|
+
└── log_user_prompt.rb
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Use this structure only when you need `Output.merge` across multiple handlers — a single-handler entrypoint is just noise; register the hook class directly instead (see [Hook file structure](#hook-file-structure) above). See [`example_dotclaude/hooks/entrypoints/session_end.rb`](example_dotclaude/hooks/entrypoints/session_end.rb) for a working two-handler entrypoint.
|
|
262
|
+
|
|
263
263
|
## 🪝 Hook Types
|
|
264
264
|
|
|
265
265
|
The framework supports the following hook types:
|
|
@@ -314,37 +314,11 @@ graph LR
|
|
|
314
314
|
A[Hook triggers] --> B[JSON from STDIN] --> C[Hook does its thing] --> D[JSON to STDOUT or STDERR<br />Exit Code] --> E[Yields back to Claude Code] --> A
|
|
315
315
|
```
|
|
316
316
|
|
|
317
|
-
The main issue is that there are many different types of hooks and they each have different expectations regarding the data outputted to `
|
|
317
|
+
The main issue is that there are many different types of hooks and they each have different expectations regarding the data outputted to `STDOUT` or `STDERR` and Claude Code will react differently for each specific exit code used depending on the hook type. This DSL handles all of that for you.
|
|
318
318
|
|
|
319
|
-
###
|
|
319
|
+
### Basic hook structure
|
|
320
320
|
|
|
321
|
-
|
|
322
|
-
2. Claude Code calls the entrypoint script (e.g., `hooks/entrypoints/pre_tool_use.rb`)
|
|
323
|
-
3. The entrypoint script reads STDIN and coordinates multiple **hook handlers**
|
|
324
|
-
4. Each **hook handler** executes and returns its output data
|
|
325
|
-
5. The entrypoint script combines/processes outputs from multiple **hook handlers**
|
|
326
|
-
6. And then returns final response to Claude Code with the correct exit code
|
|
327
|
-
|
|
328
|
-
```mermaid
|
|
329
|
-
graph TD
|
|
330
|
-
A[🔧 Hook Configuration<br/>settings.json] --> B
|
|
331
|
-
B[🤖 Claude Code<br/><em>User submits prompt</em>] --> C[📋 Entrypoint<br />entrypoints/user_prompt_submit.rb]
|
|
332
|
-
|
|
333
|
-
C --> D[📋 Entrypoint<br />Parses JSON from STDIN]
|
|
334
|
-
D --> E[📋 Entrypoint<br />Calls hook handlers]
|
|
335
|
-
|
|
336
|
-
E --> F[📝 Handler<br />AppendContextRules.call<br/><em>Returns output</em>]
|
|
337
|
-
E --> G[📝 Handler<br />PromptGuard.call<br/><em>Returns output</em>]
|
|
338
|
-
|
|
339
|
-
F --> J[📋 Entrypoint<br />Calls _ClaudeHooks::Output::UserPromptSubmit.merge_ to 🔀 merge outputs]
|
|
340
|
-
G --> J
|
|
341
|
-
|
|
342
|
-
J --> K[📋 Entrypoint<br />- Writes output to STDOUT or STDERR<br />- Uses correct exit code]
|
|
343
|
-
K --> L[🤖 Yields back to Claude Code]
|
|
344
|
-
L --> B
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### Basic Hook Handler Structure
|
|
321
|
+
The simplest pattern is a single file: define your hook class, call `CLI.run_hook`. It handles STDIN parsing, error handling, and correct exit codes.
|
|
348
322
|
|
|
349
323
|
```ruby
|
|
350
324
|
#!/usr/bin/env ruby
|
|
@@ -353,47 +327,53 @@ require 'claude_hooks'
|
|
|
353
327
|
|
|
354
328
|
class AddContextAfterPrompt < ClaudeHooks::UserPromptSubmit
|
|
355
329
|
def call
|
|
356
|
-
#
|
|
357
|
-
log do
|
|
358
|
-
"--- INPUT DATA ---"
|
|
359
|
-
"session_id: #{session_id}"
|
|
360
|
-
"cwd: #{cwd}"
|
|
361
|
-
"hook_event_name: #{hook_event_name}"
|
|
362
|
-
"prompt: #{current_prompt}"
|
|
363
|
-
"---"
|
|
364
|
-
end
|
|
365
|
-
|
|
330
|
+
log "session_id: #{session_id}, prompt: #{prompt}"
|
|
366
331
|
log "Full conversation transcript: #{read_transcript}"
|
|
367
332
|
|
|
368
|
-
# Use a Hook state method to modify what's sent back to Claude Code
|
|
369
333
|
add_additional_context!("Some custom context")
|
|
370
334
|
|
|
371
|
-
|
|
372
|
-
if current_prompt.include?("bad word")
|
|
335
|
+
if prompt.include?("bad word")
|
|
373
336
|
block_prompt!("Hmm no no no!")
|
|
374
|
-
log "Prompt blocked: #{current_prompt} because of bad word"
|
|
375
337
|
end
|
|
376
338
|
|
|
377
|
-
# Return output if you need it
|
|
378
339
|
output
|
|
379
340
|
end
|
|
380
341
|
end
|
|
381
342
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
# Read Claude Code's input data from STDIN
|
|
385
|
-
input_data = JSON.parse(STDIN.read)
|
|
343
|
+
ClaudeHooks::CLI.run_hook(AddContextAfterPrompt)
|
|
344
|
+
```
|
|
386
345
|
|
|
387
|
-
|
|
388
|
-
# Call the hook
|
|
389
|
-
hook.call
|
|
346
|
+
### Multi-handler flow
|
|
390
347
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
348
|
+
When multiple hook classes need to respond to the same event, use an entrypoint file to coordinate them:
|
|
349
|
+
|
|
350
|
+
1. A hook is registered in `~/.claude/settings.json`
|
|
351
|
+
2. Claude Code calls an entrypoint script
|
|
352
|
+
3. The entrypoint instantiates each handler and calls them
|
|
353
|
+
4. Outputs are merged with `Output.merge` (most restrictive behavior wins)
|
|
354
|
+
5. The merged output is returned to Claude Code with the correct exit code
|
|
355
|
+
|
|
356
|
+
```mermaid
|
|
357
|
+
graph TD
|
|
358
|
+
A[🔧 Hook Configuration<br/>settings.json] --> B
|
|
359
|
+
B[🤖 Claude Code<br/><em>User submits prompt</em>] --> C[📋 Entrypoint<br />entrypoints/user_prompt_submit.rb]
|
|
360
|
+
|
|
361
|
+
C --> D[📋 Entrypoint<br />Parses JSON from STDIN]
|
|
362
|
+
D --> E[📋 Entrypoint<br />Calls hook handlers]
|
|
363
|
+
|
|
364
|
+
E --> F[📝 Handler<br />AppendRules.call<br/><em>Returns output</em>]
|
|
365
|
+
E --> G[📝 Handler<br />LogUserPrompt.call<br/><em>Returns output</em>]
|
|
366
|
+
|
|
367
|
+
F --> J[📋 Entrypoint<br />Calls _ClaudeHooks::Output::UserPromptSubmit.merge_ to 🔀 merge outputs]
|
|
368
|
+
G --> J
|
|
369
|
+
|
|
370
|
+
J --> K[📋 Entrypoint<br />- Writes output to STDOUT or STDERR<br />- Uses correct exit code]
|
|
371
|
+
K --> L[🤖 Yields back to Claude Code]
|
|
372
|
+
L --> B
|
|
395
373
|
```
|
|
396
374
|
|
|
375
|
+
See [Hook Output Merging](#-hook-output-merging) below for the entrypoint code that implements this flow, and [`example_dotclaude/hooks/entrypoints/user_prompt_submit.rb`](example_dotclaude/hooks/entrypoints/user_prompt_submit.rb) for a working `AppendRules` + `LogUserPrompt` example.
|
|
376
|
+
|
|
397
377
|
## 📚 API Reference
|
|
398
378
|
|
|
399
379
|
The goal of those APIs is to simplify reading from `STDIN` and writing to `STDOUT` or `STDERR` as well as exiting with the right exit codes: the way Claude Code expects you to.
|
|
@@ -522,7 +502,7 @@ Logs are written to session-specific files in the configured log directory:
|
|
|
522
502
|
|
|
523
503
|
Let's create a hook that will monitor tool usage and ask for permission before using dangerous tools.
|
|
524
504
|
|
|
525
|
-
First, register
|
|
505
|
+
First, register your hook in `~/.claude/settings.json`:
|
|
526
506
|
|
|
527
507
|
```json
|
|
528
508
|
"hooks": {
|
|
@@ -532,7 +512,7 @@ First, register an entrypoint in `~/.claude/settings.json`:
|
|
|
532
512
|
"hooks": [
|
|
533
513
|
{
|
|
534
514
|
"type": "command",
|
|
535
|
-
"command": "~/.claude/hooks/
|
|
515
|
+
"command": "~/.claude/hooks/tool_monitor.rb"
|
|
536
516
|
}
|
|
537
517
|
]
|
|
538
518
|
}
|
|
@@ -540,43 +520,11 @@ First, register an entrypoint in `~/.claude/settings.json`:
|
|
|
540
520
|
}
|
|
541
521
|
```
|
|
542
522
|
|
|
543
|
-
Then
|
|
544
|
-
```bash
|
|
545
|
-
touch ~/.claude/hooks/entrypoints/pre_tool_use.rb
|
|
546
|
-
chmod +x ~/.claude/hooks/entrypoints/pre_tool_use.rb
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
```ruby
|
|
550
|
-
#!/usr/bin/env ruby
|
|
551
|
-
|
|
552
|
-
require 'json'
|
|
553
|
-
require_relative '../handlers/pre_tool_use/tool_monitor'
|
|
554
|
-
|
|
555
|
-
begin
|
|
556
|
-
# Read input from stdin
|
|
557
|
-
input_data = JSON.parse(STDIN.read)
|
|
558
|
-
|
|
559
|
-
tool_monitor = ToolMonitor.new(input_data)
|
|
560
|
-
tool_monitor.call
|
|
561
|
-
|
|
562
|
-
# You could also call any other handler here and then merge the outputs
|
|
563
|
-
|
|
564
|
-
tool_monitor.output_and_exit
|
|
565
|
-
rescue StandardError => e
|
|
566
|
-
STDERR.puts JSON.generate({
|
|
567
|
-
continue: false,
|
|
568
|
-
stopReason: "Hook execution error: #{e.message}",
|
|
569
|
-
suppressOutput: false
|
|
570
|
-
})
|
|
571
|
-
# Non-blocking error
|
|
572
|
-
exit 1
|
|
573
|
-
end
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
Finally, create the handler that will be used to monitor tool usage.
|
|
523
|
+
Then create the hook script and make it executable:
|
|
577
524
|
|
|
578
525
|
```bash
|
|
579
|
-
touch ~/.claude/hooks/
|
|
526
|
+
touch ~/.claude/hooks/tool_monitor.rb
|
|
527
|
+
chmod +x ~/.claude/hooks/tool_monitor.rb
|
|
580
528
|
```
|
|
581
529
|
|
|
582
530
|
```ruby
|
|
@@ -592,17 +540,16 @@ class ToolMonitor < ClaudeHooks::PreToolUse
|
|
|
592
540
|
|
|
593
541
|
if DANGEROUS_TOOLS.include?(tool_name)
|
|
594
542
|
log "Dangerous tool detected: #{tool_name}", level: :warn
|
|
595
|
-
# Use one of the ClaudeHooks::PreToolUse methods to modify the hook state and block the tool
|
|
596
543
|
ask_for_permission!("The tool '#{tool_name}' can impact your system. Allow?")
|
|
597
544
|
else
|
|
598
|
-
# Use one of the ClaudeHooks::PreToolUse methods to modify the hook state and allow the tool
|
|
599
545
|
approve_tool!("Safe tool usage")
|
|
600
546
|
end
|
|
601
547
|
|
|
602
|
-
# Accessor provided by ClaudeHooks::PreToolUse
|
|
603
548
|
output
|
|
604
549
|
end
|
|
605
550
|
end
|
|
551
|
+
|
|
552
|
+
ClaudeHooks::CLI.run_hook(ToolMonitor, on_error: :block)
|
|
606
553
|
```
|
|
607
554
|
|
|
608
555
|
## 🔄 Hook Output
|
|
@@ -620,24 +567,24 @@ This method will return an output object based on the hook's type class (e.g: `C
|
|
|
620
567
|
|
|
621
568
|
### 🔄 Hook Output Merging
|
|
622
569
|
|
|
623
|
-
|
|
624
|
-
Each hook type's `output` provides a `merge` method that will try to intelligently merge multiple hook results.
|
|
625
|
-
Merged outputs always inherit the **most restrictive behavior**.
|
|
570
|
+
When running multiple hooks for the same event, each hook type's `output` provides a `merge` method that intelligently combines results. Merged outputs always inherit the **most restrictive behavior**.
|
|
626
571
|
|
|
627
572
|
```ruby
|
|
573
|
+
#!/usr/bin/env ruby
|
|
628
574
|
|
|
629
575
|
require 'json'
|
|
576
|
+
require 'claude_hooks'
|
|
630
577
|
require_relative '../handlers/user_prompt_submit/hook1'
|
|
631
578
|
require_relative '../handlers/user_prompt_submit/hook2'
|
|
632
579
|
require_relative '../handlers/user_prompt_submit/hook3'
|
|
633
580
|
|
|
634
581
|
begin
|
|
635
|
-
# Read input from stdin
|
|
582
|
+
# Read input from stdin
|
|
636
583
|
input_data = JSON.parse(STDIN.read)
|
|
637
584
|
|
|
638
585
|
hook1 = Hook1.new(input_data)
|
|
639
|
-
hook2 =
|
|
640
|
-
hook3 =
|
|
586
|
+
hook2 = Hook2.new(input_data)
|
|
587
|
+
hook3 = Hook3.new(input_data)
|
|
641
588
|
|
|
642
589
|
# Execute the multiple hooks
|
|
643
590
|
hook1.call
|
|
@@ -659,6 +606,14 @@ begin
|
|
|
659
606
|
|
|
660
607
|
# Automatically handles outputting to the right stream (STDOUT or STDERR) and uses the right exit code depending on hook state
|
|
661
608
|
merged_output.output_and_exit
|
|
609
|
+
rescue StandardError => e
|
|
610
|
+
# This is exactly what CLI.run_hook does for you (non-blocking / fail-open):
|
|
611
|
+
STDERR.puts JSON.generate({
|
|
612
|
+
continue: false,
|
|
613
|
+
stopReason: "Hook execution error: #{e.message}",
|
|
614
|
+
suppressOutput: false
|
|
615
|
+
})
|
|
616
|
+
exit 1
|
|
662
617
|
end
|
|
663
618
|
```
|
|
664
619
|
|
|
@@ -781,12 +736,7 @@ class PluginFormatter < ClaudeHooks::PostToolUse
|
|
|
781
736
|
end
|
|
782
737
|
end
|
|
783
738
|
|
|
784
|
-
|
|
785
|
-
input_data = JSON.parse(STDIN.read)
|
|
786
|
-
hook = PluginFormatter.new(input_data)
|
|
787
|
-
hook.call
|
|
788
|
-
hook.output_and_exit
|
|
789
|
-
end
|
|
739
|
+
ClaudeHooks::CLI.run_hook(PluginFormatter)
|
|
790
740
|
```
|
|
791
741
|
|
|
792
742
|
**Environment variables available in plugins:**
|
|
@@ -822,7 +772,7 @@ You can use matchers to target specific MCP tools or all tools from a server:
|
|
|
822
772
|
"hooks": [
|
|
823
773
|
{
|
|
824
774
|
"type": "command",
|
|
825
|
-
"command": "~/.claude/hooks/
|
|
775
|
+
"command": "~/.claude/hooks/github_guard.rb"
|
|
826
776
|
}
|
|
827
777
|
]
|
|
828
778
|
},
|
|
@@ -831,7 +781,7 @@ You can use matchers to target specific MCP tools or all tools from a server:
|
|
|
831
781
|
"hooks": [
|
|
832
782
|
{
|
|
833
783
|
"type": "command",
|
|
834
|
-
"command": "~/.claude/hooks/
|
|
784
|
+
"command": "~/.claude/hooks/destructive_operation_guard.rb"
|
|
835
785
|
}
|
|
836
786
|
]
|
|
837
787
|
}
|
|
@@ -867,124 +817,103 @@ See the [official MCP documentation](https://modelcontextprotocol.io/) for more
|
|
|
867
817
|
|
|
868
818
|
## ⚠️ Troubleshooting
|
|
869
819
|
|
|
870
|
-
### Make your
|
|
820
|
+
### Make your hook scripts executable
|
|
871
821
|
|
|
872
822
|
Don't forget to make the scripts called from `settings.json` executable:
|
|
873
823
|
|
|
874
824
|
```bash
|
|
875
|
-
chmod +x ~/.claude/hooks/
|
|
825
|
+
chmod +x ~/.claude/hooks/my_hook.rb
|
|
876
826
|
```
|
|
877
827
|
|
|
878
828
|
|
|
879
829
|
## 🧪 CLI Debugging
|
|
880
830
|
|
|
881
|
-
|
|
831
|
+
`ClaudeHooks::CLI` provides two helpers: `run_hook` (for production use) and `test_runner`/`run_with_sample_data` (for local testing with custom input).
|
|
882
832
|
|
|
883
|
-
###
|
|
833
|
+
### CLI.run_hook
|
|
884
834
|
|
|
885
|
-
|
|
835
|
+
`CLI.run_hook` is what you put at the bottom of every simple hook script. It reads JSON from STDIN, runs your hook, handles errors, and calls `output_and_exit` with the right exit code.
|
|
886
836
|
|
|
887
837
|
```ruby
|
|
888
|
-
#
|
|
889
|
-
|
|
890
|
-
begin
|
|
891
|
-
require 'json'
|
|
892
|
-
input_data = JSON.parse(STDIN.read)
|
|
893
|
-
hook = MyHook.new(input_data)
|
|
894
|
-
result = hook.call
|
|
895
|
-
puts JSON.generate(result)
|
|
896
|
-
rescue StandardError => e
|
|
897
|
-
STDERR.puts "Error: #{e.message}"
|
|
898
|
-
puts JSON.generate({
|
|
899
|
-
continue: false,
|
|
900
|
-
stopReason: "Error: #{e.message}",
|
|
901
|
-
suppressOutput: false
|
|
902
|
-
})
|
|
903
|
-
exit 1
|
|
904
|
-
end
|
|
905
|
-
end
|
|
838
|
+
# Single hook (most common)
|
|
839
|
+
ClaudeHooks::CLI.run_hook(MyHook)
|
|
906
840
|
```
|
|
907
841
|
|
|
908
|
-
|
|
842
|
+
It replaces the more verbose
|
|
909
843
|
|
|
910
844
|
```ruby
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
845
|
+
begin
|
|
846
|
+
# Read input from stdin
|
|
847
|
+
input_data = JSON.parse(STDIN.read)
|
|
848
|
+
|
|
849
|
+
hook = MyHook.new(input_data)
|
|
850
|
+
hook.call
|
|
851
|
+
hook.output_and_exit
|
|
852
|
+
rescue StandardError => e
|
|
853
|
+
# Non-blocking by default (fail-open): Claude continues as if the hook didn't run.
|
|
854
|
+
# Pass `on_error: :block` to CLI.run_hook to exit 2 (fail-closed) instead.
|
|
855
|
+
STDERR.puts JSON.generate({
|
|
856
|
+
continue: false,
|
|
857
|
+
stopReason: "Hook execution error: #{e.message}",
|
|
858
|
+
suppressOutput: false
|
|
859
|
+
})
|
|
860
|
+
exit 1
|
|
914
861
|
end
|
|
915
862
|
```
|
|
916
863
|
|
|
917
|
-
|
|
864
|
+
#### on_error: fail-open vs fail-closed
|
|
918
865
|
|
|
919
|
-
|
|
866
|
+
By default, if your hook raises an unexpected exception, `CLI.run_hook` exits 1 (non-blocking) — Claude continues as if the hook didn't run. This is **fail-open**.
|
|
920
867
|
|
|
921
|
-
|
|
922
|
-
if __FILE__ == $0
|
|
923
|
-
ClaudeHooks::CLI.test_runner(MyHook) do |input_data|
|
|
924
|
-
input_data['debug_mode'] = true
|
|
925
|
-
input_data['custom_field'] = 'test_value'
|
|
926
|
-
input_data['user_name'] = 'TestUser'
|
|
927
|
-
end
|
|
928
|
-
end
|
|
929
|
-
```
|
|
930
|
-
|
|
931
|
-
### Testing Methods
|
|
868
|
+
For security or policy hooks (`PreToolUse` guards, prompt filters, etc.) you almost certainly want **fail-closed** instead — a crash should block the action, not silently pass it through:
|
|
932
869
|
|
|
933
|
-
#### 1. Test with STDIN (default)
|
|
934
870
|
```ruby
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
```
|
|
871
|
+
# Default: hook crash is non-blocking — Claude continues anyway (exit 1)
|
|
872
|
+
ClaudeHooks::CLI.run_hook(MyHook)
|
|
938
873
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
ClaudeHooks::CLI.run_with_sample_data(MyHook, { 'prompt' => 'test prompt' })
|
|
942
|
-
# Provides default values, no STDIN needed
|
|
943
|
-
```
|
|
874
|
+
# Fail-closed: hook crash blocks the action (exit 2)
|
|
875
|
+
ClaudeHooks::CLI.run_hook(MyHook, on_error: :block)
|
|
944
876
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
input_data['prompt'] = 'Custom test prompt'
|
|
949
|
-
input_data['debug'] = true
|
|
877
|
+
# Also works with block form
|
|
878
|
+
ClaudeHooks::CLI.run_hook(on_error: :block) do |input_data|
|
|
879
|
+
# ...
|
|
950
880
|
end
|
|
951
881
|
```
|
|
952
882
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
```ruby
|
|
956
|
-
#!/usr/bin/env ruby
|
|
957
|
-
|
|
958
|
-
require 'claude_hooks'
|
|
883
|
+
> [!WARNING]
|
|
884
|
+
> If you're writing a `PreToolUse` or `UserPromptSubmit` hook that enforces security policy, use `on_error: :block`. Without it, a Ruby exception (network timeout, nil reference, etc.) will silently allow the action through.
|
|
959
885
|
|
|
960
|
-
|
|
961
|
-
def call
|
|
962
|
-
log "Debug mode: #{input_data['debug_mode']}"
|
|
963
|
-
log "Processing: #{prompt}"
|
|
886
|
+
### CLI.test_runner — local testing
|
|
964
887
|
|
|
965
|
-
|
|
966
|
-
log "All input keys: #{input_data.keys.join(', ')}"
|
|
967
|
-
end
|
|
888
|
+
Use `test_runner` when running the script directly (outside of Claude Code) to inject custom input data:
|
|
968
889
|
|
|
969
|
-
|
|
890
|
+
```ruby
|
|
891
|
+
# At the bottom of your hook file, guarded so it only runs directly:
|
|
892
|
+
if __FILE__ == $0
|
|
893
|
+
ClaudeHooks::CLI.test_runner(MyHook) do |input_data|
|
|
894
|
+
input_data['debug_mode'] = true
|
|
895
|
+
input_data['prompt'] = 'Test prompt'
|
|
970
896
|
end
|
|
971
897
|
end
|
|
972
898
|
|
|
973
|
-
#
|
|
899
|
+
# Or test with synthetic data (no STDIN needed):
|
|
974
900
|
if __FILE__ == $0
|
|
975
|
-
ClaudeHooks::CLI.
|
|
976
|
-
input_data['debug_mode'] = true
|
|
977
|
-
end
|
|
901
|
+
ClaudeHooks::CLI.run_with_sample_data(MyHook, { 'prompt' => 'test prompt' })
|
|
978
902
|
end
|
|
979
903
|
```
|
|
980
904
|
|
|
905
|
+
Test with real STDIN:
|
|
906
|
+
```bash
|
|
907
|
+
echo '{"session_id":"test","prompt":"Hello"}' | ruby my_hook.rb
|
|
908
|
+
```
|
|
909
|
+
|
|
981
910
|
## 🐛 Debugging
|
|
982
911
|
|
|
983
|
-
### Test
|
|
912
|
+
### Test a hook script directly
|
|
984
913
|
|
|
985
914
|
```bash
|
|
986
915
|
# Test with sample data
|
|
987
|
-
echo '{"session_id": "test", "transcript_path": "/tmp/transcript", "cwd": "/tmp", "hook_event_name": "UserPromptSubmit", "user_prompt": "Hello Claude"}' | CLAUDE_PROJECT_DIR=$(pwd) ruby ~/.claude/hooks/
|
|
916
|
+
echo '{"session_id": "test", "transcript_path": "/tmp/transcript", "cwd": "/tmp", "hook_event_name": "UserPromptSubmit", "user_prompt": "Hello Claude"}' | CLAUDE_PROJECT_DIR=$(pwd) ruby ~/.claude/hooks/user_prompt_submit.rb
|
|
988
917
|
```
|
|
989
918
|
|
|
990
919
|
## 🧪 Development & Contributing
|
|
@@ -169,18 +169,17 @@ merged_output.output_and_exit # Handles everything automatically
|
|
|
169
169
|
|
|
170
170
|
## Simplified Entrypoint Pattern
|
|
171
171
|
|
|
172
|
+
> [!NOTE]
|
|
173
|
+
> As of 1.2.1, `CLI.entrypoint` is deprecated in favour of `CLI.run_hook`. The examples below use the current API. `CLI.entrypoint` still works (with a deprecation warning) but will be removed in a future minor version.
|
|
174
|
+
|
|
172
175
|
### For Single Hooks
|
|
173
176
|
```ruby
|
|
174
177
|
#!/usr/bin/env ruby
|
|
175
178
|
require 'claude_hooks'
|
|
176
179
|
require_relative '../handlers/my_hook'
|
|
177
180
|
|
|
178
|
-
# Super simple - just
|
|
179
|
-
ClaudeHooks::CLI.
|
|
180
|
-
hook = MyHook.new(input_data)
|
|
181
|
-
hook.call
|
|
182
|
-
hook.output_and_exit
|
|
183
|
-
end
|
|
181
|
+
# Super simple - just 1 line!
|
|
182
|
+
ClaudeHooks::CLI.run_hook(MyHook)
|
|
184
183
|
```
|
|
185
184
|
|
|
186
185
|
### For Multiple Hooks with Merging
|
|
@@ -190,7 +189,7 @@ require 'claude_hooks'
|
|
|
190
189
|
require_relative '../handlers/hook1'
|
|
191
190
|
require_relative '../handlers/hook2'
|
|
192
191
|
|
|
193
|
-
ClaudeHooks::CLI.
|
|
192
|
+
ClaudeHooks::CLI.run_hook do |input_data|
|
|
194
193
|
hook1 = Hook1.new(input_data)
|
|
195
194
|
hook2 = Hook2.new(input_data)
|
|
196
195
|
hook1.call
|
|
@@ -1,35 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'claude_hooks'
|
|
4
4
|
require_relative '../handlers/session_end/cleanup_handler'
|
|
5
5
|
require_relative '../handlers/session_end/log_session_stats'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
# Read input from stdin
|
|
9
|
-
input_data = JSON.parse(STDIN.read)
|
|
10
|
-
|
|
11
|
-
# Initialize handlers
|
|
7
|
+
ClaudeHooks::CLI.run_hook do |input_data|
|
|
12
8
|
cleanup_handler = CleanupHandler.new(input_data)
|
|
13
9
|
log_handler = LogSessionStats.new(input_data)
|
|
14
10
|
|
|
15
|
-
# Execute handlers
|
|
16
11
|
cleanup_handler.call
|
|
17
12
|
log_handler.call
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
merged_output = ClaudeHooks::Output::SessionEnd.merge(
|
|
14
|
+
ClaudeHooks::Output::SessionEnd.merge(
|
|
21
15
|
cleanup_handler.output,
|
|
22
16
|
log_handler.output
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
# Output result and exit with appropriate code
|
|
26
|
-
merged_output.output_and_exit
|
|
27
|
-
|
|
28
|
-
rescue StandardError => e
|
|
29
|
-
STDERR.puts JSON.generate({
|
|
30
|
-
continue: false,
|
|
31
|
-
stopReason: "Hook execution error: #{e.message}",
|
|
32
|
-
suppressOutput: false
|
|
33
|
-
})
|
|
34
|
-
exit 2
|
|
35
|
-
end
|
|
17
|
+
).output_and_exit
|
|
18
|
+
end
|
|
@@ -1,40 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
# Example of the NEW simplified entrypoint pattern using output objects
|
|
4
|
-
# Compare this to the existing user_prompt_submit.rb to see the difference!
|
|
5
|
-
|
|
6
3
|
require 'claude_hooks'
|
|
7
|
-
require 'json'
|
|
8
|
-
# Require the output classes
|
|
9
|
-
require_relative '../../../lib/claude_hooks/output/base'
|
|
10
|
-
require_relative '../../../lib/claude_hooks/output/user_prompt_submit'
|
|
11
4
|
require_relative '../handlers/user_prompt_submit/append_rules'
|
|
12
5
|
require_relative '../handlers/user_prompt_submit/log_user_prompt'
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
# Read input from stdin
|
|
16
|
-
input_data = JSON.parse(STDIN.read)
|
|
17
|
-
|
|
18
|
-
# Execute all hook scripts
|
|
7
|
+
ClaudeHooks::CLI.run_hook do |input_data|
|
|
19
8
|
append_rules = AppendRules.new(input_data)
|
|
20
9
|
append_rules.call
|
|
21
10
|
|
|
22
11
|
log_user_prompt = LogUserPrompt.new(input_data)
|
|
23
12
|
log_user_prompt.call
|
|
24
13
|
|
|
25
|
-
|
|
14
|
+
ClaudeHooks::Output::UserPromptSubmit.merge(
|
|
26
15
|
append_rules.output,
|
|
27
16
|
log_user_prompt.output
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
merged_output.output_and_exit
|
|
31
|
-
|
|
32
|
-
rescue StandardError => e
|
|
33
|
-
# Same simple error pattern
|
|
34
|
-
STDERR.puts JSON.generate({
|
|
35
|
-
continue: false,
|
|
36
|
-
stopReason: "Hook execution error: #{e.message} #{e.backtrace.join("\n")}",
|
|
37
|
-
suppressOutput: false
|
|
38
|
-
})
|
|
39
|
-
exit 2
|
|
40
|
-
end
|
|
17
|
+
).output_and_exit
|
|
18
|
+
end
|
|
@@ -238,16 +238,10 @@ class GithubGuard < ClaudeHooks::PreToolUse
|
|
|
238
238
|
end
|
|
239
239
|
end
|
|
240
240
|
|
|
241
|
-
#
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
'hook_event_name' => 'PreToolUse',
|
|
249
|
-
'tool_name' => 'mcp__github__create_pull_request',
|
|
250
|
-
'tool_input' => { 'draft' => false },
|
|
251
|
-
)
|
|
252
|
-
end
|
|
253
|
-
end
|
|
241
|
+
# Registered directly in settings.json under PreToolUse. Claude Code runs this file
|
|
242
|
+
# with the hook payload on STDIN. on_error: :block makes the guard fail-closed — a
|
|
243
|
+
# crash blocks the tool instead of silently allowing it.
|
|
244
|
+
#
|
|
245
|
+
# Debug it by piping sample JSON:
|
|
246
|
+
# echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git push --force"}}' | ruby github_guard.rb
|
|
247
|
+
ClaudeHooks::CLI.run_hook(GithubGuard, on_error: :block)
|
data/lib/claude_hooks/cli.rb
CHANGED
|
@@ -3,191 +3,126 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
5
|
module ClaudeHooks
|
|
6
|
-
# CLI utility for testing hook handlers in isolation
|
|
7
|
-
# This module provides a standardized way to run hooks directly from the command line
|
|
8
|
-
# for testing and debugging purposes.
|
|
9
6
|
module CLI
|
|
10
7
|
class << self
|
|
11
|
-
# Run a hook
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
8
|
+
# Run a hook script from the command line.
|
|
9
|
+
# Reads JSON from STDIN, calls hook.call, and exits with the correct code.
|
|
10
|
+
#
|
|
11
|
+
# on_error controls what happens when the hook raises an unexpected exception:
|
|
12
|
+
# :allow (default) — exit 1, non-blocking; Claude continues as if the hook didn't run.
|
|
13
|
+
# :block — exit 2, blocking; Claude stops and shows the error. Use this for
|
|
14
|
+
# security/policy hooks where a crash should never silently pass through.
|
|
15
|
+
#
|
|
16
|
+
# Usage patterns:
|
|
17
|
+
#
|
|
18
|
+
# 1. Single hook class:
|
|
19
|
+
# ClaudeHooks::CLI.run_hook(MyHook)
|
|
20
|
+
# ClaudeHooks::CLI.run_hook(MyHook, on_error: :block) # fail-closed
|
|
21
|
+
#
|
|
22
|
+
# 2. Multiple hooks with merging:
|
|
23
|
+
# ClaudeHooks::CLI.run_hook(on_error: :block) do |input_data|
|
|
24
|
+
# hook1 = Hook1.new(input_data)
|
|
25
|
+
# hook2 = Hook2.new(input_data)
|
|
26
|
+
# hook1.call
|
|
27
|
+
# hook2.call
|
|
28
|
+
# ClaudeHooks::Output::PreToolUse.merge(hook1.output, hook2.output).output_and_exit
|
|
29
|
+
# end
|
|
30
|
+
def run_hook(hook_class = nil, on_error: :allow, &block)
|
|
31
|
+
input_data = JSON.parse(STDIN.read)
|
|
32
|
+
|
|
25
33
|
if block_given?
|
|
26
34
|
yield(input_data)
|
|
35
|
+
elsif hook_class
|
|
36
|
+
hook = hook_class.new(input_data)
|
|
37
|
+
hook.call
|
|
38
|
+
hook.output_and_exit
|
|
39
|
+
else
|
|
40
|
+
raise ArgumentError, "Either provide a hook_class or a block"
|
|
27
41
|
end
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# Output the result as JSON (same format as production hooks)
|
|
34
|
-
puts JSON.generate(result) if result
|
|
35
|
-
|
|
36
|
-
result
|
|
42
|
+
|
|
43
|
+
rescue JSON::ParserError => e
|
|
44
|
+
handle_run_error("JSON parsing error: #{e.message}", on_error)
|
|
45
|
+
|
|
37
46
|
rescue StandardError => e
|
|
38
|
-
|
|
47
|
+
handle_run_error("Hook execution error: #{e.message}", on_error, backtrace: e.backtrace)
|
|
39
48
|
end
|
|
40
49
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
# input_data['user_name'] = 'TestUser'
|
|
51
|
-
# end
|
|
50
|
+
# @deprecated Use {run_hook} instead.
|
|
51
|
+
def entrypoint(hook_class = nil, on_error: :allow, &block)
|
|
52
|
+
warn "[ClaudeHooks] CLI.entrypoint is deprecated — use CLI.run_hook instead."
|
|
53
|
+
run_hook(hook_class, on_error: on_error, &block)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Testing helpers — use these inside `if __FILE__ == $0` blocks, not in production.
|
|
57
|
+
|
|
58
|
+
# Run a hook with input read from STDIN, with optional block to mutate input_data before running.
|
|
52
59
|
def test_runner(hook_class, &block)
|
|
53
60
|
input_data = read_stdin_input
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if block_given?
|
|
57
|
-
yield(input_data)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
run_hook(hook_class, input_data)
|
|
61
|
+
yield(input_data) if block_given?
|
|
62
|
+
run_hook_with_data(hook_class, input_data)
|
|
61
63
|
end
|
|
62
64
|
|
|
63
|
-
# Run hook with sample data (
|
|
64
|
-
# Usage:
|
|
65
|
-
# ClaudeHooks::CLI.run_with_sample_data(YourHookClass)
|
|
66
|
-
# ClaudeHooks::CLI.run_with_sample_data(YourHookClass, { 'prompt' => 'test prompt' })
|
|
67
|
-
#
|
|
68
|
-
# # With customization block:
|
|
69
|
-
# ClaudeHooks::CLI.run_with_sample_data(YourHookClass) do |input_data|
|
|
70
|
-
# input_data['prompt'] = 'Custom test prompt'
|
|
71
|
-
# input_data['debug'] = true
|
|
72
|
-
# end
|
|
65
|
+
# Run a hook with synthetic sample data (no STDIN needed).
|
|
73
66
|
def run_with_sample_data(hook_class, sample_data = {}, &block)
|
|
74
|
-
|
|
67
|
+
input_data = {
|
|
75
68
|
'session_id' => 'test-session',
|
|
76
69
|
'transcript_path' => '/tmp/test_transcript.md',
|
|
77
70
|
'cwd' => Dir.pwd,
|
|
78
71
|
'hook_event_name' => hook_class.hook_type
|
|
79
|
-
}
|
|
72
|
+
}.merge(sample_data)
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# Apply customization block if provided
|
|
85
|
-
if block_given?
|
|
86
|
-
yield(merged_data)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
run_hook(hook_class, merged_data)
|
|
74
|
+
yield(input_data) if block_given?
|
|
75
|
+
run_hook_with_data(hook_class, input_data)
|
|
90
76
|
end
|
|
91
77
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# hook.output_and_exit
|
|
102
|
-
# end
|
|
103
|
-
#
|
|
104
|
-
# 2. Simple form - single hook class:
|
|
105
|
-
# ClaudeHooks::CLI.entrypoint(MyHook)
|
|
106
|
-
#
|
|
107
|
-
# 3. Multiple hooks with merging:
|
|
108
|
-
# ClaudeHooks::CLI.entrypoint do |input_data|
|
|
109
|
-
# hook1 = Hook1.new(input_data)
|
|
110
|
-
# hook2 = Hook2.new(input_data)
|
|
111
|
-
# result1 = hook1.call
|
|
112
|
-
# result2 = hook2.call
|
|
113
|
-
#
|
|
114
|
-
# # Use the appropriate output class for merging
|
|
115
|
-
# merged = ClaudeHooks::Output::PreToolUse.merge(
|
|
116
|
-
# hook1.output,
|
|
117
|
-
# hook2.output
|
|
118
|
-
# )
|
|
119
|
-
# merged.output_and_exit
|
|
120
|
-
# end
|
|
121
|
-
def entrypoint(hook_class = nil, &block)
|
|
122
|
-
# Read and parse input from STDIN
|
|
123
|
-
input_data = JSON.parse(STDIN.read)
|
|
124
|
-
|
|
125
|
-
if block_given?
|
|
126
|
-
# Custom block form
|
|
127
|
-
yield(input_data)
|
|
128
|
-
elsif hook_class
|
|
129
|
-
# Simple single hook form
|
|
130
|
-
hook = hook_class.new(input_data)
|
|
131
|
-
hook.call
|
|
132
|
-
hook.output_and_exit
|
|
133
|
-
else
|
|
134
|
-
raise ArgumentError, "Either provide a hook_class or a block"
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
rescue JSON::ParserError => e
|
|
138
|
-
STDERR.puts "JSON parsing error: #{e.message}"
|
|
139
|
-
error_response = {
|
|
140
|
-
continue: false,
|
|
141
|
-
stopReason: "JSON parsing error: #{e.message}",
|
|
142
|
-
suppressOutput: false
|
|
143
|
-
}
|
|
144
|
-
response = JSON.generate(error_response)
|
|
145
|
-
puts response
|
|
146
|
-
STDERR.puts response
|
|
147
|
-
exit 1
|
|
148
|
-
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Runs a hook with already-parsed input_data. Returns the result without exiting.
|
|
81
|
+
# Used internally by test_runner and run_with_sample_data.
|
|
82
|
+
def run_hook_with_data(hook_class, input_data)
|
|
83
|
+
hook = hook_class.new(input_data)
|
|
84
|
+
result = hook.call
|
|
85
|
+
puts JSON.generate(result) if result
|
|
86
|
+
result
|
|
149
87
|
rescue StandardError => e
|
|
150
|
-
|
|
88
|
+
hook_name = hook_class.name || hook_class.to_s
|
|
89
|
+
STDERR.puts "Error in #{hook_name} hook: #{e.message}"
|
|
151
90
|
STDERR.puts e.backtrace.join("\n") if e.backtrace
|
|
152
|
-
|
|
153
|
-
error_response = {
|
|
91
|
+
response = JSON.generate({
|
|
154
92
|
continue: false,
|
|
155
|
-
stopReason: "
|
|
93
|
+
stopReason: "#{hook_name} execution error: #{e.message}",
|
|
156
94
|
suppressOutput: false
|
|
157
|
-
}
|
|
158
|
-
response = JSON.generate(error_response)
|
|
95
|
+
})
|
|
159
96
|
puts response
|
|
160
97
|
STDERR.puts response
|
|
161
98
|
exit 1
|
|
162
99
|
end
|
|
163
100
|
|
|
164
|
-
|
|
101
|
+
def handle_run_error(message, on_error, backtrace: nil)
|
|
102
|
+
if on_error == :block
|
|
103
|
+
# Exit 2: Claude Code shows stderr to the model as plain text (never
|
|
104
|
+
# parsed as JSON), so emit just the message and block.
|
|
105
|
+
STDERR.puts message
|
|
106
|
+
exit 2
|
|
107
|
+
else
|
|
108
|
+
# Exit 1: non-blocking. stderr's first line surfaces in the transcript.
|
|
109
|
+
STDERR.puts backtrace.join("\n") if backtrace
|
|
110
|
+
STDERR.puts JSON.generate({
|
|
111
|
+
continue: false,
|
|
112
|
+
stopReason: message,
|
|
113
|
+
suppressOutput: false
|
|
114
|
+
})
|
|
115
|
+
exit 1
|
|
116
|
+
end
|
|
117
|
+
end
|
|
165
118
|
|
|
166
119
|
def read_stdin_input
|
|
167
120
|
stdin_content = STDIN.read.strip
|
|
168
121
|
return {} if stdin_content.empty?
|
|
169
|
-
|
|
170
122
|
JSON.parse(stdin_content)
|
|
171
123
|
rescue JSON::ParserError => e
|
|
172
124
|
raise "Invalid JSON input: #{e.message}"
|
|
173
125
|
end
|
|
174
|
-
|
|
175
|
-
def handle_error(error, hook_class)
|
|
176
|
-
STDERR.puts "Error in #{hook_class.name} hook: #{error.message}"
|
|
177
|
-
STDERR.puts error.backtrace.join("\n") if error.backtrace
|
|
178
|
-
|
|
179
|
-
# Output error response in Claude Code format
|
|
180
|
-
error_response = {
|
|
181
|
-
continue: false,
|
|
182
|
-
stopReason: "#{hook_class.name} execution error: #{error.message}",
|
|
183
|
-
suppressOutput: false
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
response = JSON.generate(error_response)
|
|
187
|
-
puts response
|
|
188
|
-
STDERR.puts response
|
|
189
|
-
exit 1
|
|
190
|
-
end
|
|
191
126
|
end
|
|
192
127
|
end
|
|
193
|
-
end
|
|
128
|
+
end
|
data/lib/claude_hooks/version.rb
CHANGED
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.2.
|
|
4
|
+
version: 1.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriel Dehan
|
|
@@ -64,7 +64,6 @@ files:
|
|
|
64
64
|
- ".agents/skills/ci-monitoring/SKILL.md"
|
|
65
65
|
- ".agents/skills/run-tests/SKILL.md"
|
|
66
66
|
- ".claude/auto-fix.md"
|
|
67
|
-
- ".claude/settings.local.json"
|
|
68
67
|
- AGENTS.md
|
|
69
68
|
- CHANGELOG.md
|
|
70
69
|
- README.md
|
|
@@ -110,10 +109,9 @@ files:
|
|
|
110
109
|
- docs/mitts/setup.md
|
|
111
110
|
- docs/mitts/task.md
|
|
112
111
|
- example_dotclaude/commands/.gitkeep
|
|
113
|
-
- example_dotclaude/hooks/entrypoints/pre_tool_use.rb
|
|
114
112
|
- example_dotclaude/hooks/entrypoints/session_end.rb
|
|
115
113
|
- example_dotclaude/hooks/entrypoints/user_prompt_submit.rb
|
|
116
|
-
- example_dotclaude/hooks/
|
|
114
|
+
- example_dotclaude/hooks/github_guard.rb
|
|
117
115
|
- example_dotclaude/hooks/handlers/session_end/cleanup_handler.rb
|
|
118
116
|
- example_dotclaude/hooks/handlers/session_end/log_session_stats.rb
|
|
119
117
|
- example_dotclaude/hooks/handlers/user_prompt_submit/append_rules.rb
|
data/.claude/settings.local.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"env": {
|
|
3
|
-
"CLAUDE_CODE_SUBAGENT_MODEL": "sonnet"
|
|
4
|
-
},
|
|
5
|
-
"permissions": {
|
|
6
|
-
"allow": [
|
|
7
|
-
"Read(//Users/gdehan/**)",
|
|
8
|
-
"Read(//Users/gdehan/.config/**)",
|
|
9
|
-
"Bash(herdr --help)",
|
|
10
|
-
"Bash(herdr config *)",
|
|
11
|
-
"Bash(herdr api *)",
|
|
12
|
-
"Bash(python3 -m json.tool)",
|
|
13
|
-
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(list\\(d.keys\\(\\)\\)\\)\")",
|
|
14
|
-
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(list\\(d.keys\\(\\)\\), indent=2\\)\\)\")",
|
|
15
|
-
"Bash(python3 -c ' *)",
|
|
16
|
-
"Bash(herdr server *)",
|
|
17
|
-
"WebFetch(domain:opencode.ai)",
|
|
18
|
-
"Bash(ruby *)"
|
|
19
|
-
]
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require 'claude_hooks'
|
|
5
|
-
require_relative '../handlers/pre_tool_use/github_guard'
|
|
6
|
-
|
|
7
|
-
begin
|
|
8
|
-
# Read Claude Code input from stdin
|
|
9
|
-
input_data = JSON.parse($stdin.read)
|
|
10
|
-
|
|
11
|
-
github_guard = GithubGuard.new(input_data)
|
|
12
|
-
github_guard.call
|
|
13
|
-
|
|
14
|
-
github_guard.output_and_exit
|
|
15
|
-
rescue StandardError => e
|
|
16
|
-
puts JSON.generate(
|
|
17
|
-
{
|
|
18
|
-
continue: false,
|
|
19
|
-
stopReason: "Error in PreToolUse hook, #{e.message}, #{e.backtrace.join("\n")}",
|
|
20
|
-
suppressOutput: false,
|
|
21
|
-
},
|
|
22
|
-
)
|
|
23
|
-
# Allow anyway, to not block developers if there is an issue with the hook
|
|
24
|
-
exit 1
|
|
25
|
-
end
|