claude_hooks 0.1.1 → 0.2.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/CHANGELOG.md +33 -0
- data/README.md +223 -68
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit.rb +3 -12
- data/example_dotclaude/hooks/handlers/user_prompt_submit/append_rules.rb +2 -23
- data/example_dotclaude/hooks/handlers/user_prompt_submit/log_user_prompt.rb +1 -14
- data/lib/claude_hooks/base.rb +20 -4
- data/lib/claude_hooks/cli.rb +119 -0
- data/lib/claude_hooks/configuration.rb +106 -15
- data/lib/claude_hooks/version.rb +2 -2
- data/lib/claude_hooks.rb +1 -0
- metadata +3 -4
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit/append_rules.rb +0 -66
- data/example_dotclaude/hooks/entrypoints/user_prompt_submit/log_user_prompt.rb +0 -50
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 518b315ef2a184008b91029cdb9735b77ceee67490618d9581be16ff0c42cdb2
|
4
|
+
data.tar.gz: c469281a8f36dce611c1aad3f60611d5951219d1718846739d711fc310219c6f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c500ced26412c6666d19cbef211afe43b89dcaa1b6282f6fdeb5ec24874981621d3ef7b742a1c3fc45d9f68c91e838d0c16a6e90fb63c81a6f90facfb0f23e11
|
7
|
+
data.tar.gz: db4d7cc2ca66d8d6cb7c3582cda5433f2b6882998f600a43cda170f58951246fd5394704b4ce6136c461138160c509d0e5bbec3390530329b754b11aa62911dc
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,39 @@ 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
|
+
## [0.2.0] - 2025-08-21
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- **Dual Configuration System**: Support for both home-level (`$HOME/.claude`) and project-level (`$CLAUDE_PROJECT_DIR/.claude`) configurations
|
12
|
+
- **Configuration Merging**: Intelligent merging of home and project configs with configurable precedence
|
13
|
+
- New environment variable `CLAUDE_HOOKS_CONFIG_MERGE_STRATEGY` to control merge behavior ("project" or "home")
|
14
|
+
- New directory access methods: `home_claude_dir`, `project_claude_dir`
|
15
|
+
- New path utility methods: `home_path_for(path)`, `project_path_for(path)`
|
16
|
+
- Enhanced `path_for(path, base_dir=nil)` method with optional base directory parameter
|
17
|
+
- Comprehensive test suite for configuration functionality (`test/` directory)
|
18
|
+
- Configuration validation and edge case handling for missing `CLAUDE_PROJECT_DIR`
|
19
|
+
|
20
|
+
### Changed
|
21
|
+
- **Logs Location**: Logs now always go to `$HOME/.claude/{logDirectory}` regardless of active configuration
|
22
|
+
- Configuration loading now supports dual config file locations with intelligent merging
|
23
|
+
- Enhanced documentation with comprehensive dual configuration examples
|
24
|
+
- Updated API reference with new directory and path methods
|
25
|
+
|
26
|
+
### Deprecated
|
27
|
+
- `base_dir` method (still functional for backward compatibility)
|
28
|
+
- `RUBY_CLAUDE_HOOKS_BASE_DIR` environment variable (still supported as fallback)
|
29
|
+
|
30
|
+
### Fixed
|
31
|
+
- Graceful handling of undefined `CLAUDE_PROJECT_DIR` environment variable
|
32
|
+
- Proper path resolution when project directory is not available
|
33
|
+
- Backward compatibility maintained for all existing hook scripts
|
34
|
+
|
35
|
+
### Migration Notes
|
36
|
+
- Existing configurations continue to work without changes
|
37
|
+
- New projects can leverage dual configuration system
|
38
|
+
- `base_dir` and legacy `path_for` methods remain functional
|
39
|
+
- Environment variables maintain same precedence over config files
|
40
|
+
|
8
41
|
## [0.1.0] - 2025-08-17
|
9
42
|
|
10
43
|
### Added
|
data/README.md
CHANGED
@@ -4,6 +4,8 @@ A Ruby DSL (Domain Specific Language) for creating Claude Code hooks. This will
|
|
4
4
|
|
5
5
|
[**Why use this instead of writing bash, or simple ruby scripts?**](WHY.md)
|
6
6
|
|
7
|
+
> You might also be interested in my other project, a [Claude Code statusline](https://github.com/gabriel-dehan/claude_monitor_statusline) that shows your Claude usage in realtime ✨.
|
8
|
+
|
7
9
|
## 🚀 Quick Start
|
8
10
|
|
9
11
|
> [!TIP]
|
@@ -19,6 +21,7 @@ Here's how to create a simple hook:
|
|
19
21
|
1. **Create a simple hook script**
|
20
22
|
```ruby
|
21
23
|
#!/usr/bin/env ruby
|
24
|
+
require 'json'
|
22
25
|
require 'claude_hooks'
|
23
26
|
|
24
27
|
# Inherit from the right hook type class to get access to helper methods
|
@@ -43,10 +46,11 @@ if __FILE__ == $0
|
|
43
46
|
end
|
44
47
|
```
|
45
48
|
|
46
|
-
3. ⚠️ **Make it executable
|
49
|
+
3. ⚠️ **Make it executable**
|
47
50
|
```bash
|
48
51
|
chmod +x add_context_after_prompt.rb
|
49
|
-
|
52
|
+
# Test it
|
53
|
+
echo '{"session_id":"test","prompt":"Hello!"}' | ./add_context_after_prompt.rb
|
50
54
|
```
|
51
55
|
|
52
56
|
4. **Register it in your `.claude/settings.json`**
|
@@ -72,83 +76,120 @@ That's it! Your hook will now add context to every user prompt. 🎉
|
|
72
76
|
|
73
77
|
## 📦 Installation
|
74
78
|
|
75
|
-
|
79
|
+
Install it globally (simpler):
|
80
|
+
|
81
|
+
```bash
|
82
|
+
$ gem install claude_hooks
|
83
|
+
```
|
84
|
+
|
85
|
+
**Note:** Claude Code itself will still use the system-installed gem, not the bundled version unless you use `bundle exec` to run it in your `.claude/settings.json`.
|
86
|
+
|
87
|
+
Or add it to your Gemfile (you can add a Gemfile in your `.claude` directory if needed):
|
88
|
+
|
76
89
|
|
77
90
|
```ruby
|
91
|
+
# .claude/Gemfile
|
92
|
+
source 'https://rubygems.org'
|
93
|
+
|
78
94
|
gem 'claude_hooks'
|
79
95
|
```
|
80
96
|
|
81
|
-
And then
|
97
|
+
And then run:
|
82
98
|
|
83
99
|
```bash
|
84
100
|
$ bundle install
|
85
101
|
```
|
86
102
|
|
87
|
-
|
88
|
-
|
89
|
-
```bash
|
90
|
-
$ gem install claude_hooks
|
91
|
-
```
|
103
|
+
> [!WARNING]
|
104
|
+
> If you use a Gemfile, you need to use `bundle exec` to run your hooks in your `.claude/settings.json`.
|
92
105
|
|
93
106
|
### 🔧 Configuration
|
94
107
|
|
95
|
-
|
108
|
+
Claude Hooks supports both home-level (`$HOME/.claude`) and project-level (`$CLAUDE_PROJECT_DIR/.claude`) directories. Claude Hooks specific config files (`config/config.json`) found in either directory will be merged together.
|
96
109
|
|
110
|
+
| Directory | Description | Purpose |
|
111
|
+
|-----------|-------------|---------|
|
112
|
+
| `$HOME/.claude` | Home Claude directory | Global user settings and logs |
|
113
|
+
| `$CLAUDE_PROJECT_DIR/.claude` | Project Claude directory | Project-specific settings |
|
97
114
|
|
98
|
-
|
115
|
+
> [!NOTE]
|
116
|
+
> Logs always go to `$HOME/.claude/{logDirectory}`
|
99
117
|
|
100
|
-
|
101
|
-
|--------|-------------|---------|
|
102
|
-
| `baseDir` | Base directory for all Claude files | `~/.claude` |
|
103
|
-
| `logDirectory` | Directory for logs (relative to baseDir) | `logs` |
|
118
|
+
#### Environment Variables
|
104
119
|
|
105
|
-
|
106
|
-
|
107
|
-
The gem uses environment variables with the `RUBY_CLAUDE_HOOKS_` prefix for configuration:
|
120
|
+
You can configure Claude Hooks through environment variables with the `RUBY_CLAUDE_HOOKS_` prefix:
|
108
121
|
|
109
122
|
```bash
|
110
|
-
|
111
|
-
export RUBY_CLAUDE_HOOKS_LOG_DIR="logs" # Default: logs (relative to
|
123
|
+
# Existing configuration options
|
124
|
+
export RUBY_CLAUDE_HOOKS_LOG_DIR="logs" # Default: logs (relative to HOME/.claude)
|
125
|
+
export CLAUDE_HOOKS_CONFIG_MERGE_STRATEGY="project" # Config merge strategy: "project" or "home", default: "project"
|
126
|
+
export RUBY_CLAUDE_HOOKS_BASE_DIR="~/.claude" # Deprecated: fallback base directory
|
112
127
|
|
113
|
-
#
|
128
|
+
# Any variable prefixed with RUBY_CLAUDE_HOOKS_ will also be available through the config object
|
114
129
|
export RUBY_CLAUDE_HOOKS_API_KEY="your-api-key"
|
115
130
|
export RUBY_CLAUDE_HOOKS_DEBUG_MODE="true"
|
116
131
|
export RUBY_CLAUDE_HOOKS_USER_NAME="Gabriel"
|
117
132
|
```
|
118
133
|
|
119
|
-
#### Configuration
|
134
|
+
#### Configuration Files
|
120
135
|
|
121
|
-
You can
|
122
|
-
The gem will read from it as fallback for any missing environment variables.
|
136
|
+
You can also use configuration files in any of the two locations:
|
123
137
|
|
138
|
+
**Home config** (`$HOME/.claude/config/config.json`):
|
124
139
|
```json
|
125
140
|
{
|
126
|
-
|
141
|
+
// Existing configuration option
|
127
142
|
"logDirectory": "logs",
|
128
|
-
|
129
|
-
"
|
143
|
+
// Custom configuration options
|
144
|
+
"apiKey": "your-global-api-key",
|
130
145
|
"userName": "Gabriel"
|
131
146
|
}
|
132
147
|
```
|
133
148
|
|
134
|
-
|
149
|
+
**Project config** (`$CLAUDE_PROJECT_DIR/.claude/config/config.json`):
|
150
|
+
```json
|
151
|
+
{
|
152
|
+
// Custom configuration option
|
153
|
+
"projectSpecificConfig": "someValue",
|
154
|
+
}
|
155
|
+
```
|
156
|
+
|
157
|
+
#### Configuration Merging
|
158
|
+
|
159
|
+
When both config files exist, they will be merged with configurable precedence:
|
160
|
+
|
161
|
+
- **Default (`project`)**: Project config values override home config values
|
162
|
+
- **Home precedence (`home`)**: Home config values override project config values
|
163
|
+
|
164
|
+
Set merge strategy: `export CLAUDE_HOOKS_CONFIG_MERGE_STRATEGY="home" | "project"` (default: "project")
|
165
|
+
|
166
|
+
> [!WARNING]
|
167
|
+
> Environment Variables > Merged Config Files
|
168
|
+
|
169
|
+
#### Accessing Configuration Variables
|
135
170
|
|
136
171
|
You can access any configuration value in your handlers:
|
137
172
|
|
138
173
|
```ruby
|
139
174
|
class MyHandler < ClaudeHooks::UserPromptSubmit
|
140
175
|
def call
|
141
|
-
# Access
|
142
|
-
log "
|
176
|
+
# Access directory paths
|
177
|
+
log "Home Claude dir: #{home_claude_dir}"
|
178
|
+
log "Project Claude dir: #{project_claude_dir}" # nil if CLAUDE_PROJECT_DIR not set
|
179
|
+
log "Base dir (deprecated): #{base_dir}"
|
143
180
|
log "Logs dir: #{config.logs_directory}"
|
144
181
|
|
182
|
+
# Path utilities
|
183
|
+
log "Home config path: #{home_path_for('config')}"
|
184
|
+
log "Project hooks path: #{project_path_for('hooks')}" # nil if no project dir
|
185
|
+
|
145
186
|
# Access custom config via method calls
|
146
187
|
log "API Key: #{config.api_key}"
|
147
188
|
log "Debug mode: #{config.debug_mode}"
|
148
189
|
log "User: #{config.user_name}"
|
149
190
|
|
150
191
|
# Or use get_config_value for more control
|
151
|
-
user_name = config.get_config_value('USER_NAME', 'userName'
|
192
|
+
user_name = config.get_config_value('USER_NAME', 'userName')
|
152
193
|
log "Username: #{user_name}"
|
153
194
|
|
154
195
|
output_data
|
@@ -156,18 +197,16 @@ class MyHandler < ClaudeHooks::UserPromptSubmit
|
|
156
197
|
end
|
157
198
|
```
|
158
199
|
|
159
|
-
**Configuration Priority:** Environment variables always take precedence over config file values.
|
160
|
-
|
161
200
|
## 📖 Table of Contents
|
162
201
|
|
163
202
|
- [Ruby DSL for Claude Code hooks](#ruby-dsl-for-claude-code-hooks)
|
164
203
|
- [🚀 Quick Start](#-quick-start)
|
165
204
|
- [📦 Installation](#-installation)
|
166
205
|
- [🔧 Configuration](#-configuration)
|
167
|
-
- [
|
168
|
-
- [
|
169
|
-
- [Configuration
|
170
|
-
- [Accessing
|
206
|
+
- [Environment Variables](#environment-variables)
|
207
|
+
- [Configuration Files](#configuration-files)
|
208
|
+
- [Configuration Merging](#configuration-merging)
|
209
|
+
- [Accessing Configuration Variables](#accessing-configuration-variables)
|
171
210
|
- [📖 Table of Contents](#-table-of-contents)
|
172
211
|
- [🏗️ Architecture](#️-architecture)
|
173
212
|
- [Core Components](#core-components)
|
@@ -175,7 +214,7 @@ end
|
|
175
214
|
- [🪝 Hook Types](#-hook-types)
|
176
215
|
- [🚀 Claude Hook Flow](#-claude-hook-flow)
|
177
216
|
- [A very simplified view of how a hook works in Claude Code](#a-very-simplified-view-of-how-a-hook-works-in-claude-code)
|
178
|
-
- [🔄 Claude Hook
|
217
|
+
- [🔄 Proposal: a more robust Claude Hook execution flow](#-proposal-a-more-robust-claude-hook-execution-flow)
|
179
218
|
- [Basic Hook Handler Structure](#basic-hook-handler-structure)
|
180
219
|
- [Input Fields](#input-fields)
|
181
220
|
- [📚 API Reference](#-api-reference)
|
@@ -183,7 +222,9 @@ end
|
|
183
222
|
- [Input Methods](#input-methods)
|
184
223
|
- [Output Methods](#output-methods)
|
185
224
|
- [Class Output Methods](#class-output-methods)
|
225
|
+
- [Configuration and Utility Methods](#configuration-and-utility-methods)
|
186
226
|
- [Utility Methods](#utility-methods)
|
227
|
+
- [Configuration Methods](#configuration-methods)
|
187
228
|
- [UserPromptSubmit API](#userpromptsubmit-api)
|
188
229
|
- [Input Methods](#input-methods-1)
|
189
230
|
- [Output Methods](#output-methods-1)
|
@@ -209,9 +250,6 @@ end
|
|
209
250
|
- [SessionStart API](#sessionstart-api)
|
210
251
|
- [Input Methods](#input-methods-8)
|
211
252
|
- [Output Methods](#output-methods-8)
|
212
|
-
- [Configuration and Utility Methods](#configuration-and-utility-methods)
|
213
|
-
- [Configuration Methods](#configuration-methods)
|
214
|
-
- [Utility Methods](#utility-methods-2)
|
215
253
|
- [📝 Logging](#-logging)
|
216
254
|
- [Log File Location](#log-file-location)
|
217
255
|
- [Log Output Format](#log-output-format)
|
@@ -225,6 +263,14 @@ end
|
|
225
263
|
- [🚨 Advices](#-advices)
|
226
264
|
- [⚠️ Troubleshooting](#️-troubleshooting)
|
227
265
|
- [Make your entrypoint scripts executable](#make-your-entrypoint-scripts-executable)
|
266
|
+
- [🧪 CLI Debugging](#-cli-debugging)
|
267
|
+
- [Basic Usage](#basic-usage)
|
268
|
+
- [Customization with Blocks](#customization-with-blocks)
|
269
|
+
- [Testing Methods](#testing-methods)
|
270
|
+
- [1. Test with STDIN (default)](#1-test-with-stdin-default)
|
271
|
+
- [2. Test with default sample data instead of STDIN](#2-test-with-default-sample-data-instead-of-stdin)
|
272
|
+
- [3. Test with Sample Data + Customization](#3-test-with-sample-data--customization)
|
273
|
+
- [Example Hook with CLI Testing](#example-hook-with-cli-testing)
|
228
274
|
- [🐛 Debugging](#-debugging)
|
229
275
|
- [Test an individual entrypoint](#test-an-individual-entrypoint)
|
230
276
|
|
@@ -281,10 +327,10 @@ The framework supports the following hook types:
|
|
281
327
|
|
282
328
|
```mermaid
|
283
329
|
graph LR
|
284
|
-
|
330
|
+
A[Hook triggers] --> B[JSON from STDIN] --> C[Hook does its thing] --> D[JSON to STDOUT or STDERR] --> E[Yields back to Claude Code] --> A
|
285
331
|
```
|
286
332
|
|
287
|
-
### 🔄 Claude Hook
|
333
|
+
### 🔄 Proposal: a more robust Claude Hook execution flow
|
288
334
|
|
289
335
|
1. An entrypoint for a hook is set in `~/.claude/settings.json`
|
290
336
|
2. Claude Code calls the entrypoint script (e.g., `hooks/entrypoints/pre_tool_use.rb`)
|
@@ -301,8 +347,8 @@ graph TD
|
|
301
347
|
C --> D[📋 Entrypoint<br />Parses JSON from STDIN]
|
302
348
|
D --> E[📋 Entrypoint<br />Calls hook handlers]
|
303
349
|
|
304
|
-
E --> F[📝 AppendContextRules.call<br/><em>Returns output_data</em>]
|
305
|
-
E --> G[📝 PromptGuard.call<br/><em>Returns output_data</em>]
|
350
|
+
E --> F[📝 Handler<br />AppendContextRules.call<br/><em>Returns output_data</em>]
|
351
|
+
E --> G[📝 Handler<br />PromptGuard.call<br/><em>Returns output_data</em>]
|
306
352
|
|
307
353
|
F --> J[📋 Entrypoint<br />Calls _ClaudeHooks::UserPromptSubmit.merge_outputs_ to 🔀 merge outputs]
|
308
354
|
G --> J
|
@@ -405,11 +451,30 @@ Each hook type provides a **class method** `merge_outputs` that will try to inte
|
|
405
451
|
|--------|-------------|
|
406
452
|
| `merge_outputs(*outputs_data)` | Intelligently merge multiple outputs into a single output |
|
407
453
|
|
454
|
+
### Configuration and Utility Methods
|
455
|
+
|
456
|
+
Available in all hooks via the base `ClaudeHooks::Base` class:
|
457
|
+
|
408
458
|
#### Utility Methods
|
409
459
|
| Method | Description |
|
410
460
|
|--------|-------------|
|
411
461
|
| `log(message, level: :info)` | Log to session-specific file (levels: :info, :warn, :error) |
|
412
462
|
|
463
|
+
#### Configuration Methods
|
464
|
+
| Method | Description |
|
465
|
+
|--------|-------------|
|
466
|
+
| `home_claude_dir` | Get the home Claude directory (`$HOME/.claude`) |
|
467
|
+
| `project_claude_dir` | Get the project Claude directory (`$CLAUDE_PROJECT_DIR/.claude`, or `nil`) |
|
468
|
+
| `home_path_for(relative_path)` | Get absolute path relative to home Claude directory |
|
469
|
+
| `project_path_for(relative_path)` | Get absolute path relative to project Claude directory (or `nil`) |
|
470
|
+
| `base_dir` | Get the base Claude directory (**deprecated**) |
|
471
|
+
| `path_for(relative_path, base_dir=nil)` | Get absolute path relative to specified or default base dir (**deprecated**) |
|
472
|
+
| `config` | Access the merged configuration object |
|
473
|
+
| `config.get_config_value(env_key, config_file_key, default)` | Get any config value with fallback |
|
474
|
+
| `config.logs_directory` | Get logs directory path (always under home directory) |
|
475
|
+
| `config.your_custom_key` | Access any custom config via method_missing |
|
476
|
+
|
477
|
+
|
413
478
|
### UserPromptSubmit API
|
414
479
|
|
415
480
|
Available when inheriting from `ClaudeHooks::UserPromptSubmit`:
|
@@ -543,26 +608,6 @@ Available when inheriting from `ClaudeHooks::SessionStart`:
|
|
543
608
|
| `add_context!(context)` | Alias for `add_additional_context!` |
|
544
609
|
| `empty_additional_context!` | Clear additional context |
|
545
610
|
|
546
|
-
### Configuration and Utility Methods
|
547
|
-
|
548
|
-
Available in all hooks via the base `ClaudeHooks::Base` class:
|
549
|
-
|
550
|
-
#### Configuration Methods
|
551
|
-
| Method | Description |
|
552
|
-
|--------|-------------|
|
553
|
-
| `base_dir` | Get the base Claude directory |
|
554
|
-
| `path_for(relative_path)` | Get absolute path relative to base dir |
|
555
|
-
| `config` | Access the full configuration object |
|
556
|
-
| `config.get_config_value(env_key, config_key, default)` | Get any config value with fallback |
|
557
|
-
| `config.logs_directory` | Get logs directory path |
|
558
|
-
| `config.your_custom_key` | Access any custom config via method_missing |
|
559
|
-
|
560
|
-
#### Utility Methods
|
561
|
-
| Method | Description |
|
562
|
-
|--------|-------------|
|
563
|
-
| `log(message, level: :info)` | Log to session-specific file (levels: :info, :warn, :error) |
|
564
|
-
| `log(level: :info) { block }` | Multiline logging with block support |
|
565
|
-
|
566
611
|
### 📝 Logging
|
567
612
|
|
568
613
|
`ClaudeHooks::Base` provides a **session logger** that will write logs to session-specific files.
|
@@ -579,6 +624,14 @@ log <<~TEXT
|
|
579
624
|
TEXT
|
580
625
|
```
|
581
626
|
|
627
|
+
You can also use the logger from an entrypoint script:
|
628
|
+
```ruby
|
629
|
+
require 'claude_hooks'
|
630
|
+
|
631
|
+
logger = ClaudeHooks::Logger.new("TEST-SESSION-01", 'entrypoint')
|
632
|
+
logger.log "Simple message"
|
633
|
+
```
|
634
|
+
|
582
635
|
#### Log File Location
|
583
636
|
Logs are written to session-specific files in the configured log directory:
|
584
637
|
- **Defaults to**: `~/.claude/logs/hooks/session-{session_id}.log`
|
@@ -737,7 +790,7 @@ exit 0
|
|
737
790
|
For the operation to stop for a UserPromptSubmit hook, you would return structured JSON data followed by `exit 1`:
|
738
791
|
|
739
792
|
```ruby
|
740
|
-
|
793
|
+
STDERR.puts JSON.generate({
|
741
794
|
continue: false,
|
742
795
|
stopReason: "JSON parsing error: #{e.message}",
|
743
796
|
suppressOutput: false
|
@@ -746,13 +799,13 @@ exit 1
|
|
746
799
|
```
|
747
800
|
|
748
801
|
> [!WARNING]
|
749
|
-
> Don't forget to use
|
802
|
+
> Don't forget to use `STDERR.puts` to output the JSON to STDERR.
|
750
803
|
|
751
804
|
|
752
805
|
## 🚨 Advices
|
753
806
|
|
754
807
|
1. **Logging**: Use `log()` method instead of `puts` to avoid interfering with JSON output
|
755
|
-
2. **Error Handling**: Hooks should handle their own errors and use the `log` method for debugging. For errors, don't forget to exit with the right exit code (1, 2) and output the JSON indicating the error to STDERR using
|
808
|
+
2. **Error Handling**: Hooks should handle their own errors and use the `log` method for debugging. For errors, don't forget to exit with the right exit code (1, 2) and output the JSON indicating the error to STDERR using `STDERR.puts`.
|
756
809
|
3. **Output Format**: Always return `output_data` or `nil` from your `call` method
|
757
810
|
4. **Path Management**: Use `path_for()` for all file operations relative to the Claude base directory
|
758
811
|
|
@@ -767,6 +820,108 @@ chmod +x ~/.claude/hooks/entrypoints/user_prompt_submit.rb
|
|
767
820
|
```
|
768
821
|
|
769
822
|
|
823
|
+
## 🧪 CLI Debugging
|
824
|
+
|
825
|
+
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.
|
826
|
+
|
827
|
+
### Basic Usage
|
828
|
+
|
829
|
+
Replace the traditional testing boilerplate:
|
830
|
+
|
831
|
+
```ruby
|
832
|
+
# Old way (15+ lines of repetitive code)
|
833
|
+
if __FILE__ == $0
|
834
|
+
begin
|
835
|
+
require 'json'
|
836
|
+
input_data = JSON.parse(STDIN.read)
|
837
|
+
hook = MyHook.new(input_data)
|
838
|
+
result = hook.call
|
839
|
+
puts JSON.generate(result)
|
840
|
+
rescue StandardError => e
|
841
|
+
STDERR.puts "Error: #{e.message}"
|
842
|
+
puts JSON.generate({
|
843
|
+
continue: false,
|
844
|
+
stopReason: "Error: #{e.message}",
|
845
|
+
suppressOutput: false
|
846
|
+
})
|
847
|
+
exit 1
|
848
|
+
end
|
849
|
+
end
|
850
|
+
```
|
851
|
+
|
852
|
+
With the simple CLI test runner:
|
853
|
+
|
854
|
+
```ruby
|
855
|
+
# New way (1 line!)
|
856
|
+
if __FILE__ == $0
|
857
|
+
ClaudeHooks::CLI.test_runner(MyHook)
|
858
|
+
end
|
859
|
+
```
|
860
|
+
|
861
|
+
### Customization with Blocks
|
862
|
+
|
863
|
+
You can customize the input data for testing using blocks:
|
864
|
+
|
865
|
+
```ruby
|
866
|
+
if __FILE__ == $0
|
867
|
+
ClaudeHooks::CLI.test_runner(MyHook) do |input_data|
|
868
|
+
input_data['debug_mode'] = true
|
869
|
+
input_data['custom_field'] = 'test_value'
|
870
|
+
input_data['user_name'] = 'TestUser'
|
871
|
+
end
|
872
|
+
end
|
873
|
+
```
|
874
|
+
|
875
|
+
### Testing Methods
|
876
|
+
|
877
|
+
#### 1. Test with STDIN (default)
|
878
|
+
```ruby
|
879
|
+
ClaudeHooks::CLI.test_runner(MyHook)
|
880
|
+
# Usage: echo '{"session_id":"test","prompt":"Hello"}' | ruby my_hook.rb
|
881
|
+
```
|
882
|
+
|
883
|
+
#### 2. Test with default sample data instead of STDIN
|
884
|
+
```ruby
|
885
|
+
ClaudeHooks::CLI.run_with_sample_data(MyHook, { 'prompt' => 'test prompt' })
|
886
|
+
# Provides default values, no STDIN needed
|
887
|
+
```
|
888
|
+
|
889
|
+
#### 3. Test with Sample Data + Customization
|
890
|
+
```ruby
|
891
|
+
ClaudeHooks::CLI.run_with_sample_data(MyHook) do |input_data|
|
892
|
+
input_data['prompt'] = 'Custom test prompt'
|
893
|
+
input_data['debug'] = true
|
894
|
+
end
|
895
|
+
```
|
896
|
+
|
897
|
+
### Example Hook with CLI Testing
|
898
|
+
|
899
|
+
```ruby
|
900
|
+
#!/usr/bin/env ruby
|
901
|
+
|
902
|
+
require 'claude_hooks'
|
903
|
+
|
904
|
+
class MyTestHook < ClaudeHooks::UserPromptSubmit
|
905
|
+
def call
|
906
|
+
log "Debug mode: #{input_data['debug_mode']}"
|
907
|
+
log "Processing: #{prompt}"
|
908
|
+
|
909
|
+
if input_data['debug_mode']
|
910
|
+
log "All input keys: #{input_data.keys.join(', ')}"
|
911
|
+
end
|
912
|
+
|
913
|
+
output_data
|
914
|
+
end
|
915
|
+
end
|
916
|
+
|
917
|
+
# Test runner with customization
|
918
|
+
if __FILE__ == $0
|
919
|
+
ClaudeHooks::CLI.test_runner(MyTestHook) do |input_data|
|
920
|
+
input_data['debug_mode'] = true
|
921
|
+
end
|
922
|
+
end
|
923
|
+
```
|
924
|
+
|
770
925
|
## 🐛 Debugging
|
771
926
|
|
772
927
|
### Test an individual entrypoint
|
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'claude_hooks'
|
4
4
|
require 'json'
|
5
|
-
require_relative '../user_prompt_submit/append_rules'
|
6
|
-
require_relative '../user_prompt_submit/log_user_prompt'
|
5
|
+
require_relative '../handlers/user_prompt_submit/append_rules'
|
6
|
+
require_relative '../handlers/user_prompt_submit/log_user_prompt'
|
7
7
|
|
8
8
|
begin
|
9
9
|
# Read input from stdin
|
@@ -22,16 +22,7 @@ begin
|
|
22
22
|
# Output final merged result to Claude Code
|
23
23
|
puts JSON.generate(hook_output)
|
24
24
|
|
25
|
-
exit 0
|
26
|
-
rescue JSON::ParserError => e
|
27
|
-
STDERR.puts "Error parsing JSON: #{e.message}"
|
28
|
-
|
29
|
-
puts JSON.generate({
|
30
|
-
continue: false,
|
31
|
-
stopReason: "Hook JSON parsing error: #{e.message}",
|
32
|
-
suppressOutput: false
|
33
|
-
})
|
34
|
-
exit 1
|
25
|
+
exit 0 # Don't forget to exit with the right exit code (0, 1, 2)
|
35
26
|
rescue StandardError => e
|
36
27
|
STDERR.puts "Error in UserPromptSubmit hook: #{e.message} #{e.backtrace.join("\n")}"
|
37
28
|
|
@@ -39,28 +39,7 @@ end
|
|
39
39
|
|
40
40
|
# If this file is run directly (for testing), call the hook script
|
41
41
|
if __FILE__ == $0
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
input_data = JSON.parse(STDIN.read)
|
46
|
-
hook = AppendRules.new(input_data)
|
47
|
-
hook.call
|
48
|
-
puts hook.stringify_output
|
49
|
-
rescue JSON::ParserError => e
|
50
|
-
STDERR.puts "Error parsing JSON: #{e.message}"
|
51
|
-
puts JSON.generate({
|
52
|
-
continue: false,
|
53
|
-
stopReason: "JSON parsing error in AppendRules: #{e.message}",
|
54
|
-
suppressOutput: false
|
55
|
-
})
|
56
|
-
exit 0
|
57
|
-
rescue StandardError => e
|
58
|
-
STDERR.puts "Error in AppendRules hook: #{e.message}, #{e.backtrace.join("\n")}"
|
59
|
-
puts JSON.generate({
|
60
|
-
continue: false,
|
61
|
-
stopReason: "AppendRules execution error: #{e.message}",
|
62
|
-
suppressOutput: false
|
63
|
-
})
|
64
|
-
exit 0
|
42
|
+
ClaudeHooks::CLI.test_runner(AppendRules) do |input_data|
|
43
|
+
input_data['session_id'] = 'session-id-override-01'
|
65
44
|
end
|
66
45
|
end
|
@@ -33,18 +33,5 @@ end
|
|
33
33
|
|
34
34
|
# If this file is run directly (for testing), call the hook
|
35
35
|
if __FILE__ == $0
|
36
|
-
|
37
|
-
require 'json'
|
38
|
-
|
39
|
-
hook = LogUserPrompt.new(JSON.parse(STDIN.read))
|
40
|
-
hook.call
|
41
|
-
rescue StandardError => e
|
42
|
-
STDERR.puts "Error in LogUserPrompt hook: #{e.message}, #{e.backtrace.join("\n")}"
|
43
|
-
puts JSON.generate({
|
44
|
-
continue: false,
|
45
|
-
stopReason: "LogUserPrompt execution error: #{e.message}",
|
46
|
-
suppressOutput: false
|
47
|
-
})
|
48
|
-
exit 0
|
49
|
-
end
|
36
|
+
ClaudeHooks::CLI.test_runner(LogUserPrompt)
|
50
37
|
end
|
data/lib/claude_hooks/base.rb
CHANGED
@@ -26,7 +26,7 @@ module ClaudeHooks
|
|
26
26
|
|
27
27
|
attr_reader :config, :input_data, :output_data, :logger
|
28
28
|
def initialize(input_data = {})
|
29
|
-
@config = Configuration
|
29
|
+
@config = Configuration
|
30
30
|
@input_data = input_data
|
31
31
|
@output_data = {
|
32
32
|
'continue' => true,
|
@@ -108,11 +108,27 @@ module ClaudeHooks
|
|
108
108
|
# === CONFIG AND UTILITY METHODS ===
|
109
109
|
|
110
110
|
def base_dir
|
111
|
-
|
111
|
+
config.base_dir
|
112
112
|
end
|
113
113
|
|
114
|
-
def
|
115
|
-
|
114
|
+
def home_claude_dir
|
115
|
+
config.home_claude_dir
|
116
|
+
end
|
117
|
+
|
118
|
+
def project_claude_dir
|
119
|
+
config.project_claude_dir
|
120
|
+
end
|
121
|
+
|
122
|
+
def path_for(relative_path, base_directory = nil)
|
123
|
+
config.path_for(relative_path, base_directory)
|
124
|
+
end
|
125
|
+
|
126
|
+
def home_path_for(relative_path)
|
127
|
+
config.home_path_for(relative_path)
|
128
|
+
end
|
129
|
+
|
130
|
+
def project_path_for(relative_path)
|
131
|
+
config.project_path_for(relative_path)
|
116
132
|
end
|
117
133
|
|
118
134
|
# Supports both single messages and blocks for multiline logging
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
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
|
+
module CLI
|
10
|
+
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
|
25
|
+
if block_given?
|
26
|
+
yield(input_data)
|
27
|
+
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
|
37
|
+
rescue StandardError => e
|
38
|
+
handle_error(e, hook_class)
|
39
|
+
end
|
40
|
+
|
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
|
52
|
+
def test_runner(hook_class, &block)
|
53
|
+
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
|
+
end
|
62
|
+
|
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
|
73
|
+
def run_with_sample_data(hook_class, sample_data = {}, &block)
|
74
|
+
default_sample = {
|
75
|
+
'session_id' => 'test-session',
|
76
|
+
'transcript_path' => '/tmp/test_transcript.md',
|
77
|
+
'cwd' => Dir.pwd,
|
78
|
+
'hook_event_name' => hook_class.hook_type
|
79
|
+
}
|
80
|
+
|
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)
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def read_stdin_input
|
95
|
+
stdin_content = STDIN.read.strip
|
96
|
+
return {} if stdin_content.empty?
|
97
|
+
|
98
|
+
JSON.parse(stdin_content)
|
99
|
+
rescue JSON::ParserError => e
|
100
|
+
raise "Invalid JSON input: #{e.message}"
|
101
|
+
end
|
102
|
+
|
103
|
+
def handle_error(error, hook_class)
|
104
|
+
STDERR.puts "Error in #{hook_class.name} hook: #{error.message}"
|
105
|
+
STDERR.puts error.backtrace.join("\n") if error.backtrace
|
106
|
+
|
107
|
+
# Output error response in Claude Code format
|
108
|
+
error_response = {
|
109
|
+
continue: false,
|
110
|
+
stopReason: "#{hook_class.name} execution error: #{error.message}",
|
111
|
+
suppressOutput: false
|
112
|
+
}
|
113
|
+
|
114
|
+
puts JSON.generate(error_response)
|
115
|
+
exit 1
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -16,29 +16,75 @@ module ClaudeHooks
|
|
16
16
|
def reload!
|
17
17
|
@config = nil
|
18
18
|
@base_dir = nil
|
19
|
+
@home_claude_dir = nil
|
20
|
+
@project_claude_dir = nil
|
19
21
|
@config_file_path = nil
|
22
|
+
@home_config_file_path = nil
|
23
|
+
@project_config_file_path = nil
|
20
24
|
end
|
21
25
|
|
22
|
-
# Get the
|
26
|
+
# Get the home Claude directory (always ~/.claude)
|
27
|
+
def home_claude_dir
|
28
|
+
@home_claude_dir ||= File.expand_path('~/.claude')
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the project Claude directory (from CLAUDE_PROJECT_DIR/.claude)
|
32
|
+
# Returns nil if CLAUDE_PROJECT_DIR environment variable is not set
|
33
|
+
def project_claude_dir
|
34
|
+
@project_claude_dir ||= begin
|
35
|
+
project_dir = ENV['CLAUDE_PROJECT_DIR']
|
36
|
+
if project_dir
|
37
|
+
File.expand_path(File.join(project_dir, '.claude'))
|
38
|
+
else
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get the base directory from ENV or default (backward compatibility)
|
45
|
+
# This method will determine which base directory to use based on context
|
23
46
|
def base_dir
|
24
47
|
@base_dir ||= begin
|
48
|
+
# Check for legacy environment variable first
|
25
49
|
env_base_dir = ENV["#{ENV_PREFIX}BASE_DIR"]
|
26
|
-
|
50
|
+
if env_base_dir
|
51
|
+
File.expand_path(env_base_dir)
|
52
|
+
else
|
53
|
+
# Default to home directory for backward compatibility
|
54
|
+
home_claude_dir
|
55
|
+
end
|
27
56
|
end
|
28
57
|
end
|
29
58
|
|
30
59
|
# Get the full path for a file/directory relative to base_dir
|
31
|
-
|
32
|
-
|
60
|
+
# Can optionally specify which base directory to use
|
61
|
+
def path_for(relative_path, base_directory = nil)
|
62
|
+
base_directory ||= base_dir
|
63
|
+
File.join(base_directory, relative_path)
|
33
64
|
end
|
34
65
|
|
35
|
-
# Get the
|
66
|
+
# Get the full path for a file/directory relative to home_claude_dir
|
67
|
+
def home_path_for(relative_path)
|
68
|
+
File.join(home_claude_dir, relative_path)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get the full path for a file/directory relative to project_claude_dir
|
72
|
+
# Returns nil if CLAUDE_PROJECT_DIR environment variable is not set
|
73
|
+
def project_path_for(relative_path)
|
74
|
+
if project_claude_dir
|
75
|
+
File.join(project_claude_dir, relative_path)
|
76
|
+
else
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get the log directory path (always relative to home_claude_dir)
|
36
82
|
def logs_directory
|
37
83
|
log_dir = get_config_value('LOG_DIR', 'logDirectory') || 'logs'
|
38
84
|
if log_dir.start_with?('/')
|
39
85
|
log_dir # Absolute path
|
40
86
|
else
|
41
|
-
|
87
|
+
File.join(home_claude_dir, log_dir) # Always relative to home_claude_dir
|
42
88
|
end
|
43
89
|
end
|
44
90
|
|
@@ -63,7 +109,8 @@ module ClaudeHooks
|
|
63
109
|
def method_missing(method_name, *args, &block)
|
64
110
|
# Convert method name to ENV key format (e.g., my_custom_setting -> MY_CUSTOM_SETTING)
|
65
111
|
env_key = method_name.to_s.upcase
|
66
|
-
|
112
|
+
# Convert snake_case method name to camelCase for config file lookup
|
113
|
+
config_key = snake_case_to_camel_case(method_name.to_s)
|
67
114
|
|
68
115
|
value = get_config_value(env_key, config_key)
|
69
116
|
return value unless value.nil?
|
@@ -74,36 +121,80 @@ module ClaudeHooks
|
|
74
121
|
def respond_to_missing?(method_name, include_private = false)
|
75
122
|
# Check if we have a config value for this method
|
76
123
|
env_key = method_name.to_s.upcase
|
77
|
-
config_key = method_name.to_s
|
124
|
+
config_key = snake_case_to_camel_case(method_name.to_s)
|
78
125
|
|
79
126
|
!get_config_value(env_key, config_key).nil? || super
|
80
127
|
end
|
81
128
|
|
82
129
|
private
|
83
130
|
|
131
|
+
def snake_case_to_camel_case(snake_str)
|
132
|
+
# Convert snake_case to camelCase (e.g., user_name -> userName)
|
133
|
+
parts = snake_str.split('_')
|
134
|
+
parts.first + parts[1..-1].map(&:capitalize).join
|
135
|
+
end
|
136
|
+
|
84
137
|
def config_file_path
|
85
138
|
@config_file_path ||= path_for('config/config.json')
|
86
139
|
end
|
87
140
|
|
141
|
+
def home_config_file_path
|
142
|
+
@home_config_file_path ||= File.join(home_claude_dir, 'config/config.json')
|
143
|
+
end
|
144
|
+
|
145
|
+
def project_config_file_path
|
146
|
+
@project_config_file_path ||= begin
|
147
|
+
if project_claude_dir
|
148
|
+
File.join(project_claude_dir, 'config/config.json')
|
149
|
+
else
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
88
155
|
def load_config
|
89
|
-
#
|
90
|
-
|
156
|
+
# Load and merge config files from both locations
|
157
|
+
merged_file_config = load_and_merge_config_files
|
91
158
|
|
92
159
|
# Merge with ENV variables
|
93
160
|
env_config = load_env_config
|
94
161
|
|
95
|
-
# ENV variables take precedence
|
96
|
-
|
162
|
+
# ENV variables take precedence over file configs
|
163
|
+
merged_file_config.merge(env_config)
|
164
|
+
end
|
165
|
+
|
166
|
+
def load_and_merge_config_files
|
167
|
+
home_config = load_config_file_from_path(home_config_file_path)
|
168
|
+
project_config = load_config_file_from_path(project_config_file_path) if project_config_file_path
|
169
|
+
|
170
|
+
# Determine merge strategy
|
171
|
+
merge_strategy = ENV['CLAUDE_HOOKS_CONFIG_MERGE_STRATEGY'] || 'project'
|
172
|
+
|
173
|
+
if project_config && merge_strategy == 'project'
|
174
|
+
# Project config takes precedence
|
175
|
+
home_config.merge(project_config)
|
176
|
+
elsif project_config && merge_strategy == 'home'
|
177
|
+
# Home config takes precedence
|
178
|
+
project_config.merge(home_config)
|
179
|
+
else
|
180
|
+
# Only home config exists or no project config
|
181
|
+
home_config
|
182
|
+
end
|
97
183
|
end
|
98
184
|
|
99
185
|
def load_config_file
|
100
186
|
config_file = config_file_path
|
187
|
+
load_config_file_from_path(config_file)
|
188
|
+
end
|
189
|
+
|
190
|
+
def load_config_file_from_path(config_file_path)
|
191
|
+
return {} unless config_file_path
|
101
192
|
|
102
|
-
if File.exist?(
|
193
|
+
if File.exist?(config_file_path)
|
103
194
|
begin
|
104
|
-
JSON.parse(File.read(
|
195
|
+
JSON.parse(File.read(config_file_path))
|
105
196
|
rescue JSON::ParserError => e
|
106
|
-
warn "Warning: Error parsing config file #{
|
197
|
+
warn "Warning: Error parsing config file #{config_file_path}: #{e.message}"
|
107
198
|
{}
|
108
199
|
end
|
109
200
|
else
|
data/lib/claude_hooks/version.rb
CHANGED
data/lib/claude_hooks.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative "claude_hooks/version"
|
|
4
4
|
require_relative "claude_hooks/configuration"
|
5
5
|
require_relative "claude_hooks/logger"
|
6
6
|
require_relative "claude_hooks/base"
|
7
|
+
require_relative "claude_hooks/cli"
|
7
8
|
require_relative "claude_hooks/user_prompt_submit"
|
8
9
|
require_relative "claude_hooks/pre_tool_use"
|
9
10
|
require_relative "claude_hooks/post_tool_use"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: claude_hooks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gabriel Dehan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -67,13 +67,12 @@ files:
|
|
67
67
|
- claude_hooks.gemspec
|
68
68
|
- example_dotclaude/commands/.gitkeep
|
69
69
|
- example_dotclaude/hooks/entrypoints/user_prompt_submit.rb
|
70
|
-
- example_dotclaude/hooks/entrypoints/user_prompt_submit/append_rules.rb
|
71
|
-
- example_dotclaude/hooks/entrypoints/user_prompt_submit/log_user_prompt.rb
|
72
70
|
- example_dotclaude/hooks/handlers/user_prompt_submit/append_rules.rb
|
73
71
|
- example_dotclaude/hooks/handlers/user_prompt_submit/log_user_prompt.rb
|
74
72
|
- example_dotclaude/settings.json
|
75
73
|
- lib/claude_hooks.rb
|
76
74
|
- lib/claude_hooks/base.rb
|
75
|
+
- lib/claude_hooks/cli.rb
|
77
76
|
- lib/claude_hooks/configuration.rb
|
78
77
|
- lib/claude_hooks/logger.rb
|
79
78
|
- lib/claude_hooks/notification.rb
|
@@ -1,66 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'claude_hooks'
|
4
|
-
|
5
|
-
# Hook script that appends rules to user prompt
|
6
|
-
class AppendRules < ClaudeHooks::UserPromptSubmit
|
7
|
-
|
8
|
-
def call
|
9
|
-
log "Executing AppendRules hook"
|
10
|
-
|
11
|
-
# Read the rule content
|
12
|
-
rule_content = read_rule_content
|
13
|
-
|
14
|
-
if rule_content
|
15
|
-
add_additional_context!(rule_content)
|
16
|
-
log "Successfully added rule content as additional context (#{rule_content.length} characters)"
|
17
|
-
else
|
18
|
-
log "No rule content found", level: :warn
|
19
|
-
end
|
20
|
-
|
21
|
-
output_data
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def read_rule_content
|
27
|
-
rule_file_path = path_for('rules/post-user-prompt.rule.md')
|
28
|
-
|
29
|
-
if File.exist?(rule_file_path)
|
30
|
-
content = File.read(rule_file_path).strip
|
31
|
-
return content unless content.empty?
|
32
|
-
end
|
33
|
-
|
34
|
-
log "Rule file not found or empty at: #{rule_file_path}", level: :warn
|
35
|
-
log "Base directory: #{base_dir}"
|
36
|
-
nil
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
# If this file is run directly (for testing), call the hook script
|
41
|
-
if __FILE__ == $0
|
42
|
-
begin
|
43
|
-
require 'json'
|
44
|
-
|
45
|
-
input_data = JSON.parse(STDIN.read)
|
46
|
-
hook = AppendRules.new(input_data)
|
47
|
-
hook.call
|
48
|
-
puts hook.stringify_output
|
49
|
-
rescue JSON::ParserError => e
|
50
|
-
STDERR.puts "Error parsing JSON: #{e.message}"
|
51
|
-
puts JSON.generate({
|
52
|
-
continue: false,
|
53
|
-
stopReason: "JSON parsing error in AppendRules: #{e.message}",
|
54
|
-
suppressOutput: false
|
55
|
-
})
|
56
|
-
exit 0
|
57
|
-
rescue StandardError => e
|
58
|
-
STDERR.puts "Error in AppendRules hook: #{e.message}, #{e.backtrace.join("\n")}"
|
59
|
-
puts JSON.generate({
|
60
|
-
continue: false,
|
61
|
-
stopReason: "AppendRules execution error: #{e.message}",
|
62
|
-
suppressOutput: false
|
63
|
-
})
|
64
|
-
exit 0
|
65
|
-
end
|
66
|
-
end
|
@@ -1,50 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'fileutils'
|
4
|
-
require 'claude_hooks'
|
5
|
-
|
6
|
-
# Example hook module that logs user prompts to a file
|
7
|
-
class LogUserPrompt < ClaudeHooks::UserPromptSubmit
|
8
|
-
|
9
|
-
def call
|
10
|
-
log "Executing LogUserPrompt hook"
|
11
|
-
|
12
|
-
# Log the prompt to a file (just as an example)
|
13
|
-
log_file_path = path_for('logs/user_prompts.log')
|
14
|
-
ensure_log_directory_exists
|
15
|
-
|
16
|
-
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
17
|
-
|
18
|
-
log <<~TEXT
|
19
|
-
Prompt: #{current_prompt}
|
20
|
-
Logged user prompt to #{log_file_path}
|
21
|
-
TEXT
|
22
|
-
|
23
|
-
nil
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def ensure_log_directory_exists
|
29
|
-
log_dir = path_for('logs')
|
30
|
-
FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
# If this file is run directly (for testing), call the hook
|
35
|
-
if __FILE__ == $0
|
36
|
-
begin
|
37
|
-
require 'json'
|
38
|
-
|
39
|
-
hook = LogUserPrompt.new(JSON.parse(STDIN.read))
|
40
|
-
hook.call
|
41
|
-
rescue StandardError => e
|
42
|
-
STDERR.puts "Error in LogUserPrompt hook: #{e.message}, #{e.backtrace.join("\n")}"
|
43
|
-
puts JSON.generate({
|
44
|
-
continue: false,
|
45
|
-
stopReason: "LogUserPrompt execution error: #{e.message}",
|
46
|
-
suppressOutput: false
|
47
|
-
})
|
48
|
-
exit 0
|
49
|
-
end
|
50
|
-
end
|