claude_hooks 0.1.2 → 0.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: 05073a7e1f50d4391f4afddff10e781bf70c1c1a5ec15b4d8c5cf7959827ca53
4
- data.tar.gz: a477cb849efb154ac3452150030bde9a4219c82887afae7b3fded7d9d01b482c
3
+ metadata.gz: '08b8b23bcfdeed5cf98e3477b6339f29488d6e547fd2fb10ef40947bbc3d45dc'
4
+ data.tar.gz: 20d0f599bac9dcde566829dabba1d1f3f13756181ed4ea7bbe4cfa1c0a95838a
5
5
  SHA512:
6
- metadata.gz: ca053e3783d088baebefa2ffed240a63f624e053dfbaa10e1513191b20428f152414d281a782e2a76ba24490b9d25033271c75c47f85e89785aec7570bcc92ff
7
- data.tar.gz: 54baf322b8031460dfcf92998c86950dd84f25b67d6b3e78270f5951dce81d169b7d355a1e6498c31fd33442572943c93795bce1fbbcb359b32ceecb145af2d6
6
+ metadata.gz: 7cd6ecea85c72ce39e77613f68c3dc9f689aca7611dd499482e20538bbb456e2c6b4c23bc791372173fd4b955e8d4211c3c8c29e0e97957812916f901327ce22
7
+ data.tar.gz: 93b6032c1c29c11081419d0415dae8ac5cd2494fa558af1637c081ce11d256403c1b26c49740c9aa8294c385aa311b12da2b3e5a6949e88a4a5f892b68abcf54
data/CHANGELOG.md CHANGED
@@ -5,6 +5,44 @@ 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.1] - 2025-08-21
9
+
10
+ ### Fixed
11
+ - Fixed name of environment variable for the merge strategy
12
+
13
+ ## [0.2.0] - 2025-08-21
14
+
15
+ ### Added
16
+ - **Dual Configuration System**: Support for both home-level (`$HOME/.claude`) and project-level (`$CLAUDE_PROJECT_DIR/.claude`) configurations
17
+ - **Configuration Merging**: Intelligent merging of home and project configs with configurable precedence
18
+ - New environment variable `RUBY_CLAUDE_HOOKS_CONFIG_MERGE_STRATEGY` to control merge behavior ("project" or "home")
19
+ - New directory access methods: `home_claude_dir`, `project_claude_dir`
20
+ - New path utility methods: `home_path_for(path)`, `project_path_for(path)`
21
+ - Enhanced `path_for(path, base_dir=nil)` method with optional base directory parameter
22
+ - Comprehensive test suite for configuration functionality (`test/` directory)
23
+ - Configuration validation and edge case handling for missing `CLAUDE_PROJECT_DIR`
24
+
25
+ ### Changed
26
+ - **Logs Location**: Logs now always go to `$HOME/.claude/{logDirectory}` regardless of active configuration
27
+ - Configuration loading now supports dual config file locations with intelligent merging
28
+ - Enhanced documentation with comprehensive dual configuration examples
29
+ - Updated API reference with new directory and path methods
30
+
31
+ ### Deprecated
32
+ - `base_dir` method (still functional for backward compatibility)
33
+ - `RUBY_CLAUDE_HOOKS_BASE_DIR` environment variable (still supported as fallback)
34
+
35
+ ### Fixed
36
+ - Graceful handling of undefined `CLAUDE_PROJECT_DIR` environment variable
37
+ - Proper path resolution when project directory is not available
38
+ - Backward compatibility maintained for all existing hook scripts
39
+
40
+ ### Migration Notes
41
+ - Existing configurations continue to work without changes
42
+ - New projects can leverage dual configuration system
43
+ - `base_dir` and legacy `path_for` methods remain functional
44
+ - Environment variables maintain same precedence over config files
45
+
8
46
  ## [0.1.0] - 2025-08-17
9
47
 
10
48
  ### 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, inside Claude Code ✨.
8
+
7
9
  ## 🚀 Quick Start
8
10
 
9
11
  > [!TIP]
@@ -44,10 +46,11 @@ if __FILE__ == $0
44
46
  end
45
47
  ```
46
48
 
47
- 3. ⚠️ **Make it executable (and test it)**
49
+ 3. ⚠️ **Make it executable**
48
50
  ```bash
49
51
  chmod +x add_context_after_prompt.rb
50
- echo '{"session_id":"test","prompt":"Hello!"}' | ruby add_context_after_prompt.rb
52
+ # Test it
53
+ echo '{"session_id":"test","prompt":"Hello!"}' | ./add_context_after_prompt.rb
51
54
  ```
52
55
 
53
56
  4. **Register it in your `.claude/settings.json`**
@@ -87,6 +90,7 @@ Or add it to your Gemfile (you can add a Gemfile in your `.claude` directory if
87
90
  ```ruby
88
91
  # .claude/Gemfile
89
92
  source 'https://rubygems.org'
93
+
90
94
  gem 'claude_hooks'
91
95
  ```
92
96
 
@@ -101,63 +105,92 @@ $ bundle install
101
105
 
102
106
  ### 🔧 Configuration
103
107
 
104
- This gem uses either environment variables or a global configuration file.
105
-
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.
106
109
 
107
- #### Required Configuration Options
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 |
108
114
 
109
- | Option | Description | Default |
110
- |--------|-------------|---------|
111
- | `baseDir` | Base directory for all Claude files | `~/.claude` |
112
- | `logDirectory` | Directory for logs (relative to baseDir) | `logs` |
115
+ > [!NOTE]
116
+ > Logs always go to `$HOME/.claude/{logDirectory}`
113
117
 
114
- #### Environment Variables (Preferred)
118
+ #### Environment Variables
115
119
 
116
- 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:
117
121
 
118
122
  ```bash
119
- export RUBY_CLAUDE_HOOKS_BASE_DIR="~/.claude" # Default: ~/.claude
120
- export RUBY_CLAUDE_HOOKS_LOG_DIR="logs" # Default: logs (relative to base_dir)
123
+ # Existing configuration options
124
+ export RUBY_CLAUDE_HOOKS_LOG_DIR="logs" # Default: logs (relative to HOME/.claude)
125
+ export RUBY_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
121
127
 
122
- # You can add any custom configuration
128
+ # Any variable prefixed with RUBY_CLAUDE_HOOKS_
129
+ # will also be available through the config object
123
130
  export RUBY_CLAUDE_HOOKS_API_KEY="your-api-key"
124
131
  export RUBY_CLAUDE_HOOKS_DEBUG_MODE="true"
125
132
  export RUBY_CLAUDE_HOOKS_USER_NAME="Gabriel"
126
133
  ```
127
134
 
128
- #### Configuration File
135
+ #### Configuration Files
129
136
 
130
- You can choose to use a global configuration file by setting it up in `~/.claude/config/config.json`.
131
- The gem will read from it as fallback for any missing environment variables.
137
+ You can also use configuration files in any of the two locations:
132
138
 
139
+ **Home config** (`$HOME/.claude/config/config.json`):
133
140
  ```json
134
141
  {
135
- "baseDir": "~/.claude",
142
+ // Existing configuration option
136
143
  "logDirectory": "logs",
137
- "apiKey": "your-api-key",
138
- "debugMode": true,
144
+ // Custom configuration options
145
+ "apiKey": "your-global-api-key",
139
146
  "userName": "Gabriel"
140
147
  }
141
148
  ```
142
149
 
143
- #### Accessing Custom Configuration
150
+ **Project config** (`$CLAUDE_PROJECT_DIR/.claude/config/config.json`):
151
+ ```json
152
+ {
153
+ // Custom configuration option
154
+ "projectSpecificConfig": "someValue",
155
+ }
156
+ ```
157
+
158
+ #### Configuration Merging
159
+
160
+ When both config files exist, they will be merged with configurable precedence:
161
+
162
+ - **Default (`project`)**: Project config values override home config values
163
+ - **Home precedence (`home`)**: Home config values override project config values
164
+
165
+ Set merge strategy: `export RUBY_CLAUDE_HOOKS_CONFIG_MERGE_STRATEGY="home" | "project"` (default: "project")
166
+
167
+ > [!WARNING]
168
+ > Environment Variables > Merged Config Files
169
+
170
+ #### Accessing Configuration Variables
144
171
 
145
172
  You can access any configuration value in your handlers:
146
173
 
147
174
  ```ruby
148
175
  class MyHandler < ClaudeHooks::UserPromptSubmit
149
176
  def call
150
- # Access built-in config
151
- log "Base dir: #{config.base_dir}"
177
+ # Access directory paths
178
+ log "Home Claude dir: #{home_claude_dir}"
179
+ log "Project Claude dir: #{project_claude_dir}" # nil if CLAUDE_PROJECT_DIR not set
180
+ log "Base dir (deprecated): #{base_dir}"
152
181
  log "Logs dir: #{config.logs_directory}"
153
182
 
183
+ # Path utilities
184
+ log "Home config path: #{home_path_for('config')}"
185
+ log "Project hooks path: #{project_path_for('hooks')}" # nil if no project dir
186
+
154
187
  # Access custom config via method calls
155
188
  log "API Key: #{config.api_key}"
156
189
  log "Debug mode: #{config.debug_mode}"
157
190
  log "User: #{config.user_name}"
158
191
 
159
192
  # Or use get_config_value for more control
160
- user_name = config.get_config_value('USER_NAME', 'userName', )
193
+ user_name = config.get_config_value('USER_NAME', 'userName')
161
194
  log "Username: #{user_name}"
162
195
 
163
196
  output_data
@@ -165,18 +198,16 @@ class MyHandler < ClaudeHooks::UserPromptSubmit
165
198
  end
166
199
  ```
167
200
 
168
- **Configuration Priority:** Environment variables always take precedence over config file values.
169
-
170
201
  ## 📖 Table of Contents
171
202
 
172
203
  - [Ruby DSL for Claude Code hooks](#ruby-dsl-for-claude-code-hooks)
173
204
  - [🚀 Quick Start](#-quick-start)
174
205
  - [📦 Installation](#-installation)
175
206
  - [🔧 Configuration](#-configuration)
176
- - [Required Configuration Options](#required-configuration-options)
177
- - [Environment Variables (Preferred)](#environment-variables-preferred)
178
- - [Configuration File](#configuration-file)
179
- - [Accessing Custom Configuration](#accessing-custom-configuration)
207
+ - [Environment Variables](#environment-variables)
208
+ - [Configuration Files](#configuration-files)
209
+ - [Configuration Merging](#configuration-merging)
210
+ - [Accessing Configuration Variables](#accessing-configuration-variables)
180
211
  - [📖 Table of Contents](#-table-of-contents)
181
212
  - [🏗️ Architecture](#️-architecture)
182
213
  - [Core Components](#core-components)
@@ -184,7 +215,7 @@ end
184
215
  - [🪝 Hook Types](#-hook-types)
185
216
  - [🚀 Claude Hook Flow](#-claude-hook-flow)
186
217
  - [A very simplified view of how a hook works in Claude Code](#a-very-simplified-view-of-how-a-hook-works-in-claude-code)
187
- - [🔄 Claude Hook Execution Flow](#-claude-hook-execution-flow)
218
+ - [🔄 Proposal: a more robust Claude Hook execution flow](#-proposal-a-more-robust-claude-hook-execution-flow)
188
219
  - [Basic Hook Handler Structure](#basic-hook-handler-structure)
189
220
  - [Input Fields](#input-fields)
190
221
  - [📚 API Reference](#-api-reference)
@@ -192,7 +223,9 @@ end
192
223
  - [Input Methods](#input-methods)
193
224
  - [Output Methods](#output-methods)
194
225
  - [Class Output Methods](#class-output-methods)
226
+ - [Configuration and Utility Methods](#configuration-and-utility-methods)
195
227
  - [Utility Methods](#utility-methods)
228
+ - [Configuration Methods](#configuration-methods)
196
229
  - [UserPromptSubmit API](#userpromptsubmit-api)
197
230
  - [Input Methods](#input-methods-1)
198
231
  - [Output Methods](#output-methods-1)
@@ -218,9 +251,6 @@ end
218
251
  - [SessionStart API](#sessionstart-api)
219
252
  - [Input Methods](#input-methods-8)
220
253
  - [Output Methods](#output-methods-8)
221
- - [Configuration and Utility Methods](#configuration-and-utility-methods)
222
- - [Configuration Methods](#configuration-methods)
223
- - [Utility Methods](#utility-methods-2)
224
254
  - [📝 Logging](#-logging)
225
255
  - [Log File Location](#log-file-location)
226
256
  - [Log Output Format](#log-output-format)
@@ -298,10 +328,10 @@ The framework supports the following hook types:
298
328
 
299
329
  ```mermaid
300
330
  graph LR
301
- A[Hook triggers] --> B[JSON from STDIN] --> C[Hook does its thing] --> D[JSON to STDOUT or STDERR]
331
+ 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
302
332
  ```
303
333
 
304
- ### 🔄 Claude Hook Execution Flow
334
+ ### 🔄 Proposal: a more robust Claude Hook execution flow
305
335
 
306
336
  1. An entrypoint for a hook is set in `~/.claude/settings.json`
307
337
  2. Claude Code calls the entrypoint script (e.g., `hooks/entrypoints/pre_tool_use.rb`)
@@ -318,8 +348,8 @@ graph TD
318
348
  C --> D[📋 Entrypoint<br />Parses JSON from STDIN]
319
349
  D --> E[📋 Entrypoint<br />Calls hook handlers]
320
350
 
321
- E --> F[📝 AppendContextRules.call<br/><em>Returns output_data</em>]
322
- E --> G[📝 PromptGuard.call<br/><em>Returns output_data</em>]
351
+ E --> F[📝 Handler<br />AppendContextRules.call<br/><em>Returns output_data</em>]
352
+ E --> G[📝 Handler<br />PromptGuard.call<br/><em>Returns output_data</em>]
323
353
 
324
354
  F --> J[📋 Entrypoint<br />Calls _ClaudeHooks::UserPromptSubmit.merge_outputs_ to 🔀 merge outputs]
325
355
  G --> J
@@ -422,11 +452,30 @@ Each hook type provides a **class method** `merge_outputs` that will try to inte
422
452
  |--------|-------------|
423
453
  | `merge_outputs(*outputs_data)` | Intelligently merge multiple outputs into a single output |
424
454
 
455
+ ### Configuration and Utility Methods
456
+
457
+ Available in all hooks via the base `ClaudeHooks::Base` class:
458
+
425
459
  #### Utility Methods
426
460
  | Method | Description |
427
461
  |--------|-------------|
428
462
  | `log(message, level: :info)` | Log to session-specific file (levels: :info, :warn, :error) |
429
463
 
464
+ #### Configuration Methods
465
+ | Method | Description |
466
+ |--------|-------------|
467
+ | `home_claude_dir` | Get the home Claude directory (`$HOME/.claude`) |
468
+ | `project_claude_dir` | Get the project Claude directory (`$CLAUDE_PROJECT_DIR/.claude`, or `nil`) |
469
+ | `home_path_for(relative_path)` | Get absolute path relative to home Claude directory |
470
+ | `project_path_for(relative_path)` | Get absolute path relative to project Claude directory (or `nil`) |
471
+ | `base_dir` | Get the base Claude directory (**deprecated**) |
472
+ | `path_for(relative_path, base_dir=nil)` | Get absolute path relative to specified or default base dir (**deprecated**) |
473
+ | `config` | Access the merged configuration object |
474
+ | `config.get_config_value(env_key, config_file_key, default)` | Get any config value with fallback |
475
+ | `config.logs_directory` | Get logs directory path (always under home directory) |
476
+ | `config.your_custom_key` | Access any custom config via method_missing |
477
+
478
+
430
479
  ### UserPromptSubmit API
431
480
 
432
481
  Available when inheriting from `ClaudeHooks::UserPromptSubmit`:
@@ -560,26 +609,6 @@ Available when inheriting from `ClaudeHooks::SessionStart`:
560
609
  | `add_context!(context)` | Alias for `add_additional_context!` |
561
610
  | `empty_additional_context!` | Clear additional context |
562
611
 
563
- ### Configuration and Utility Methods
564
-
565
- Available in all hooks via the base `ClaudeHooks::Base` class:
566
-
567
- #### Configuration Methods
568
- | Method | Description |
569
- |--------|-------------|
570
- | `base_dir` | Get the base Claude directory |
571
- | `path_for(relative_path)` | Get absolute path relative to base dir |
572
- | `config` | Access the full configuration object |
573
- | `config.get_config_value(env_key, config_key, default)` | Get any config value with fallback |
574
- | `config.logs_directory` | Get logs directory path |
575
- | `config.your_custom_key` | Access any custom config via method_missing |
576
-
577
- #### Utility Methods
578
- | Method | Description |
579
- |--------|-------------|
580
- | `log(message, level: :info)` | Log to session-specific file (levels: :info, :warn, :error) |
581
- | `log(level: :info) { block }` | Multiline logging with block support |
582
-
583
612
  ### 📝 Logging
584
613
 
585
614
  `ClaudeHooks::Base` provides a **session logger** that will write logs to session-specific files.
@@ -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
@@ -8,12 +8,12 @@ class AppendRules < ClaudeHooks::UserPromptSubmit
8
8
  def call
9
9
  log "Executing AppendRules hook"
10
10
 
11
- # Read the rule content
12
- rule_content = read_rule_content
11
+ # Read the rules
12
+ rules = read_rules
13
13
 
14
- if rule_content
15
- add_additional_context!(rule_content)
16
- log "Successfully added rule content as additional context (#{rule_content.length} characters)"
14
+ if rules
15
+ add_additional_context!(rules)
16
+ log "Successfully added rules as additional context (#{rules.length} characters)"
17
17
  else
18
18
  log "No rule content found", level: :warn
19
19
  end
@@ -23,8 +23,9 @@ class AppendRules < ClaudeHooks::UserPromptSubmit
23
23
 
24
24
  private
25
25
 
26
- def read_rule_content
27
- rule_file_path = path_for('rules/post-user-prompt.rule.md')
26
+ def read_rules
27
+ # If we were in the project directory, we would use project_path_for instead
28
+ rule_file_path = home_path_for('rules/post-user-prompt.rule.md')
28
29
 
29
30
  if File.exist?(rule_file_path)
30
31
  content = File.read(rule_file_path).strip
@@ -32,7 +33,8 @@ class AppendRules < ClaudeHooks::UserPromptSubmit
32
33
  end
33
34
 
34
35
  log "Rule file not found or empty at: #{rule_file_path}", level: :warn
35
- log "Base directory: #{base_dir}"
36
+ # If we were in the project directory, we would use project_claude_dir instead
37
+ log "Base directory: #{home_claude_dir}"
36
38
  nil
37
39
  end
38
40
  end
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'fileutils'
4
3
  require 'claude_hooks'
5
4
 
6
5
  # Example hook module that logs user prompts to a file
@@ -9,10 +8,6 @@ class LogUserPrompt < ClaudeHooks::UserPromptSubmit
9
8
  def call
10
9
  log "Executing LogUserPrompt hook"
11
10
 
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
11
  timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
17
12
 
18
13
  log <<~TEXT
@@ -22,13 +17,6 @@ class LogUserPrompt < ClaudeHooks::UserPromptSubmit
22
17
 
23
18
  nil
24
19
  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
20
  end
33
21
 
34
22
  # If this file is run directly (for testing), call the hook
@@ -111,8 +111,24 @@ module ClaudeHooks
111
111
  config.base_dir
112
112
  end
113
113
 
114
- def path_for(relative_path)
115
- config.path_for(relative_path)
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
@@ -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 base directory from ENV or default
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
- File.expand_path(env_base_dir || '~/.claude')
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
- def path_for(relative_path)
32
- File.join(base_dir, relative_path)
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)
64
+ end
65
+
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)
33
69
  end
34
70
 
35
- # Get the log directory path
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
- path_for(log_dir) # Relative to base_dir
87
+ File.join(home_claude_dir, log_dir) # Always relative to home_claude_dir
42
88
  end
43
89
  end
44
90
 
@@ -76,7 +122,7 @@ module ClaudeHooks
76
122
  # Check if we have a config value for this method
77
123
  env_key = method_name.to_s.upcase
78
124
  config_key = snake_case_to_camel_case(method_name.to_s)
79
-
125
+
80
126
  !get_config_value(env_key, config_key).nil? || super
81
127
  end
82
128
 
@@ -92,25 +138,63 @@ module ClaudeHooks
92
138
  @config_file_path ||= path_for('config/config.json')
93
139
  end
94
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
+
95
155
  def load_config
96
- # Start with config file
97
- file_config = load_config_file
98
-
156
+ # Load and merge config files from both locations
157
+ merged_file_config = load_and_merge_config_files
158
+
99
159
  # Merge with ENV variables
100
160
  env_config = load_env_config
101
-
102
- # ENV variables take precedence
103
- file_config.merge(env_config)
161
+
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['RUBY_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
104
183
  end
105
184
 
106
185
  def load_config_file
107
186
  config_file = config_file_path
187
+ load_config_file_from_path(config_file)
188
+ end
108
189
 
109
- if File.exist?(config_file)
190
+ def load_config_file_from_path(config_file_path)
191
+ return {} unless config_file_path
192
+
193
+ if File.exist?(config_file_path)
110
194
  begin
111
- JSON.parse(File.read(config_file))
195
+ JSON.parse(File.read(config_file_path))
112
196
  rescue JSON::ParserError => e
113
- warn "Warning: Error parsing config file #{config_file}: #{e.message}"
197
+ warn "Warning: Error parsing config file #{config_file_path}: #{e.message}"
114
198
  {}
115
199
  end
116
200
  else
@@ -121,15 +205,15 @@ module ClaudeHooks
121
205
 
122
206
  def load_env_config
123
207
  env_config = {}
124
-
208
+
125
209
  ENV.each do |key, value|
126
210
  next unless key.start_with?(ENV_PREFIX)
127
-
211
+
128
212
  # Remove prefix and convert to config key format
129
213
  config_key = env_key_to_config_key(key.sub(ENV_PREFIX, ''))
130
214
  env_config[config_key] = value
131
215
  end
132
-
216
+
133
217
  env_config
134
218
  end
135
219
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeHooks
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.1"
5
5
  end
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.1.2
4
+ version: 0.2.1
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-17 00:00:00.000000000 Z
11
+ date: 2025-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json