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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1803cc66c5f4c6e0d2ca706c4106654a2aa961e8a8e968d3198acab8ff95a7d
4
- data.tar.gz: 5dba30b7bce90d4a420cb1c83be1b9e2e2c3a1b0b1dfc42f47b0edd01e08941c
3
+ metadata.gz: 4ff845df334960263a1d36b01aeba57393609c59acbc63424572c9d7fdca7999
4
+ data.tar.gz: 70029c193102d9a63b6d6e5ff8361fcf42e506bff26435c907ec4a06010a571a
5
5
  SHA512:
6
- metadata.gz: f5b394c9005bea73bdb66db595e3783fc7cb2e67889dbf2923b945899bd8c907edf0df241a534ac4a2679c887a07538ec9e1d65d58d24f2b9313cded33e5ca18
7
- data.tar.gz: b9867904501454ef867f5be72a0c82e63de3d7bcfdfe6981c8cfbbf946fdd50cc2968671dfd341a1811736da51302c67e412e09302e295b61b30abca25790b23
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
- # Run the hook
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
- > This was a very simple example but we recommend using the entrypoints/handlers architecture [described below](#recommended-structure-for-your-claudehooks-directory) to create more complex hook systems.
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 Handler Classes** - Self-contained classes (`ClaudeHooks::UserPromptSubmit`, `ClaudeHooks::PreToolUse`, etc.)
236
- 3. **Output Classes** - `ClaudeHooks::Output::UserPromptSubmit`, etc... are output objects that handle intelligent merging of multiple outputs, as well as using the right exit codes and outputting to the proper stream (`STDIN` or `STDERR`) depending on the the hook state.
237
- 4. **Configuration** - Shared configuration management via `ClaudeHooks::Configuration`
238
- 5. **Logger** - Dedicated logging class with multiline block support
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
- ### Recommended structure for your .claude/hooks/ directory
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
- ├── entrypoints/ # Main entry points
245
- │   ├── notification.rb
246
- │   ├── pre_tool_use.rb
247
- │   ├── post_tool_use.rb
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 `STDIN` or `STDERR` and Claude Code will react differently for each specific exit code used depending on the hook type.
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
- ### 🔄 Proposal: a more robust Claude Hook execution flow
319
+ ### Basic hook structure
320
320
 
321
- 1. An entrypoint for a hook is set in `~/.claude/settings.json`
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
- # Access input data
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
- # Control execution, for instance: block the prompt
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
- # Use your handler (usually from an entrypoint file, but this is an example)
383
- if __FILE__ == $0
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
- hook = AddContextAfterPrompt.new(input_data)
388
- # Call the hook
389
- hook.call
346
+ ### Multi-handler flow
390
347
 
391
- # Uses exit code 0 (success) and outputs to STDIN if the prompt wasn't blocked
392
- # Uses exit code 2 (blocking error) and outputs to STDERR if the prompt was blocked
393
- hook.output_and_exit
394
- end
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 an entrypoint in `~/.claude/settings.json`:
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/entrypoints/pre_tool_use.rb"
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, create your main entrypoint script and _don't forget to make it executable_:
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/handlers/pre_tool_use/tool_monitor.rb
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
- Often, you will want to call multiple hooks from a same entrypoint.
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 = Hook1.new(input_data)
640
- hook3 = Hook1.new(input_data)
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
- if __FILE__ == $0
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/entrypoints/github_guard.rb"
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/entrypoints/destructive_operation_guard.rb"
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 entrypoint scripts executable
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/entrypoints/user_prompt_submit.rb
825
+ chmod +x ~/.claude/hooks/my_hook.rb
876
826
  ```
877
827
 
878
828
 
879
829
  ## 🧪 CLI Debugging
880
830
 
881
- The `ClaudeHooks::CLI` module provides utilities to simplify testing hooks in isolation. Instead of writing repetitive JSON parsing and error handling code, you can use the CLI test runner.
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
- ### Basic Usage
833
+ ### CLI.run_hook
884
834
 
885
- Replace the traditional testing boilerplate:
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
- # Old way (15+ lines of repetitive code)
889
- if __FILE__ == $0
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
- With the simple CLI test runner:
842
+ It replaces the more verbose
909
843
 
910
844
  ```ruby
911
- # New way (1 line!)
912
- if __FILE__ == $0
913
- ClaudeHooks::CLI.test_runner(MyHook)
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
- ### Customization with Blocks
864
+ #### on_error: fail-open vs fail-closed
918
865
 
919
- You can customize the input data for testing using blocks:
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
- ```ruby
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
- ClaudeHooks::CLI.test_runner(MyHook)
936
- # Usage: echo '{"session_id":"test","prompt":"Hello"}' | ruby my_hook.rb
937
- ```
871
+ # Default: hook crash is non-blocking — Claude continues anyway (exit 1)
872
+ ClaudeHooks::CLI.run_hook(MyHook)
938
873
 
939
- #### 2. Test with default sample data instead of STDIN
940
- ```ruby
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
- #### 3. Test with Sample Data + Customization
946
- ```ruby
947
- ClaudeHooks::CLI.run_with_sample_data(MyHook) do |input_data|
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
- ### Example Hook with CLI Testing
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
- class MyTestHook < ClaudeHooks::UserPromptSubmit
961
- def call
962
- log "Debug mode: #{input_data['debug_mode']}"
963
- log "Processing: #{prompt}"
886
+ ### CLI.test_runner local testing
964
887
 
965
- if input_data['debug_mode']
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
- output
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
- # Test runner with customization
899
+ # Or test with synthetic data (no STDIN needed):
974
900
  if __FILE__ == $0
975
- ClaudeHooks::CLI.test_runner(MyTestHook) do |input_data|
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 an individual entrypoint
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/entrypoints/user_prompt_submit.rb
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 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
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.entrypoint do |input_data|
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 'json'
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
- begin
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
- # Merge outputs using the SessionEnd output merger
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
- begin
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
- merged_output = ClaudeHooks::Output::UserPromptSubmit.merge(
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
- # When running this file directly (for debugging)
242
- if __FILE__ == $PROGRAM_NAME
243
- ClaudeHooks::CLI.run_with_sample_data(GithubGuard) do |data|
244
- data.merge!(
245
- 'session_id' => 'GithubGuardTest',
246
- 'transcript_path' => '',
247
- 'cwd' => Dir.pwd,
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)
@@ -10,7 +10,7 @@
10
10
  "hooks": [
11
11
  {
12
12
  "type": "command",
13
- "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/entrypoints/pre_tool_use.rb"
13
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/github_guard.rb"
14
14
  }
15
15
  ]
16
16
  }
@@ -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 class directly from command line
12
- # Usage:
13
- # ClaudeHooks::CLI.run_hook(YourHookClass)
14
- # ClaudeHooks::CLI.run_hook(YourHookClass, custom_input_data)
15
- #
16
- # # With customization block:
17
- # ClaudeHooks::CLI.run_hook(YourHookClass) do |input_data|
18
- # input_data['debug_mode'] = true
19
- # end
20
- def run_hook(hook_class, input_data = nil, &block)
21
- # If no input data provided, read from STDIN
22
- input_data ||= read_stdin_input
23
-
24
- # Apply customization block if provided
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
- # Create and execute the hook
30
- hook = hook_class.new(input_data)
31
- result = hook.call
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
- handle_error(e, hook_class)
47
+ handle_run_error("Hook execution error: #{e.message}", on_error, backtrace: e.backtrace)
39
48
  end
40
49
 
41
- # Create a test runner block for a hook class
42
- # This generates the common if __FILE__ == $0 block content
43
- #
44
- # Usage:
45
- # ClaudeHooks::CLI.test_runner(YourHookClass)
46
- #
47
- # # With customization block:
48
- # ClaudeHooks::CLI.test_runner(YourHookClass) do |input_data|
49
- # input_data['custom_field'] = 'test_value'
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
- # Apply customization block if provided
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 (useful for development)
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
- default_sample = {
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
- # Merge with hook-specific sample data
82
- merged_data = default_sample.merge(sample_data)
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
- # Simplified entrypoint helper for hook scripts
93
- # This handles all the STDIN reading, JSON parsing, error handling, and output execution
94
- #
95
- # Usage patterns:
96
- #
97
- # 1. Block form - custom logic:
98
- # ClaudeHooks::CLI.entrypoint do |input_data|
99
- # hook = MyHook.new(input_data)
100
- # hook.call
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
- STDERR.puts "Hook execution error: #{e.message}"
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: "Hook execution error: #{e.message}",
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
- private
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeHooks
4
- VERSION = "1.2.0"
4
+ VERSION = "1.2.1"
5
5
  end
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.0
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/handlers/pre_tool_use/github_guard.rb
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
@@ -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