Skip to content

Commit 6baab35

Browse files
authored
Merge pull request #216 from alex-feel/alex-feel-dev
Add config file support for hooks
2 parents d901a60 + 04f596e commit 6baab35

File tree

3 files changed

+193
-1
lines changed

3 files changed

+193
-1
lines changed

scripts/setup_environment.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3380,6 +3380,7 @@ def create_additional_settings(
33803380
matcher = hook.get('matcher', '')
33813381
hook_type = hook.get('type', 'command')
33823382
command = hook.get('command')
3383+
config = hook.get('config') # Optional config file reference
33833384

33843385
if not event or not command:
33853386
warning('Invalid hook configuration, skipping')
@@ -3435,12 +3436,28 @@ def create_additional_settings(
34353436
# For .pyw files on Windows, uv automatically uses pythonw
34363437
# Use --no-project flag to prevent uv from detecting and applying project Python requirements
34373438
full_command = f'uv run --no-project --python 3.12 {hook_path_str}'
3439+
3440+
# Append config file path if specified
3441+
if config:
3442+
# Strip query parameters from config filename
3443+
clean_config = config.split('?')[0] if '?' in config else config
3444+
config_path = claude_user_dir / 'hooks' / Path(clean_config).name
3445+
config_path_str = config_path.as_posix()
3446+
full_command = f'{full_command} {config_path_str}'
34383447
else:
34393448
# Other file - build absolute path and use as-is
34403449
# System will handle execution based on file extension (.sh, .bat, .cmd, .ps1, etc.)
34413450
hook_path = claude_user_dir / 'hooks' / Path(clean_command).name
34423451
hook_path_str = hook_path.as_posix()
34433452
full_command = hook_path_str
3453+
3454+
# Append config file path if specified
3455+
if config:
3456+
# Strip query parameters from config filename
3457+
clean_config = config.split('?')[0] if '?' in config else config
3458+
config_path = claude_user_dir / 'hooks' / Path(clean_config).name
3459+
config_path_str = config_path.as_posix()
3460+
full_command = f'{full_command} {config_path_str}'
34443461
else:
34453462
# Direct command with spaces - use as-is
34463463
full_command = command

tests/conftest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,17 @@ def sample_environment_config() -> dict[str, Any]:
5555
],
5656
'slash-commands': ['commands/test-command.md'],
5757
'hooks': {
58-
'files': ['hooks/test-hook.py'],
58+
'files': [
59+
'hooks/test-hook.py',
60+
'configs/test-hook-config.yaml',
61+
],
5962
'events': [
6063
{
6164
'event': 'PostToolUse',
6265
'matcher': 'Edit|Write',
6366
'type': 'command',
6467
'command': 'test-hook.py',
68+
'config': 'test-hook-config.yaml',
6569
},
6670
],
6771
},

tests/test_setup_environment_additional.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2345,3 +2345,174 @@ def test_refresh_path_from_registry_no_path_found(self):
23452345
with patch('setup_environment.winreg.OpenKey', side_effect=FileNotFoundError()):
23462346
result = setup_environment.refresh_path_from_registry()
23472347
assert result is False # Should return False when no PATH found
2348+
2349+
2350+
class TestHookConfigFileSupport:
2351+
"""Test hook configuration file support in create_additional_settings."""
2352+
2353+
def test_create_additional_settings_hook_with_config(self) -> None:
2354+
"""Test hook configuration with config file reference."""
2355+
with tempfile.TemporaryDirectory() as tmpdir:
2356+
claude_dir = Path(tmpdir)
2357+
hooks_dir = claude_dir / 'hooks'
2358+
hooks_dir.mkdir(parents=True, exist_ok=True)
2359+
2360+
# Create dummy hook and config files
2361+
hook_file = hooks_dir / 'protect_critical_files.py'
2362+
hook_file.write_text('#!/usr/bin/env python3\nprint("hook")')
2363+
config_file = hooks_dir / 'protect_config.yaml'
2364+
config_file.write_text('protected_files: []')
2365+
2366+
hooks = {
2367+
'files': [
2368+
'hooks/library/protect_critical_files.py',
2369+
'configs/protect_config.yaml',
2370+
],
2371+
'events': [
2372+
{
2373+
'event': 'PreToolUse',
2374+
'matcher': 'Edit|Write',
2375+
'type': 'command',
2376+
'command': 'protect_critical_files.py',
2377+
'config': 'protect_config.yaml',
2378+
},
2379+
],
2380+
}
2381+
2382+
result = setup_environment.create_additional_settings(
2383+
hooks,
2384+
claude_dir,
2385+
'test',
2386+
)
2387+
2388+
assert result is True
2389+
settings_file = claude_dir / 'test-additional-settings.json'
2390+
settings = json.loads(settings_file.read_text())
2391+
2392+
# Verify command includes config path
2393+
hook_cmd = settings['hooks']['PreToolUse'][0]['hooks'][0]['command']
2394+
assert 'protect_critical_files.py' in hook_cmd
2395+
assert 'protect_config.yaml' in hook_cmd
2396+
assert hook_cmd.endswith('protect_config.yaml')
2397+
2398+
def test_create_additional_settings_hook_without_config_backward_compat(self) -> None:
2399+
"""Test that hooks without config field still work (backward compatibility)."""
2400+
with tempfile.TemporaryDirectory() as tmpdir:
2401+
claude_dir = Path(tmpdir)
2402+
hooks_dir = claude_dir / 'hooks'
2403+
hooks_dir.mkdir(parents=True, exist_ok=True)
2404+
2405+
hook_file = hooks_dir / 'test.py'
2406+
hook_file.write_text('print("test")')
2407+
2408+
hooks = {
2409+
'files': ['hooks/test.py'],
2410+
'events': [
2411+
{
2412+
'event': 'PostToolUse',
2413+
'matcher': 'Edit',
2414+
'type': 'command',
2415+
'command': 'test.py',
2416+
# No 'config' field - backward compatibility
2417+
},
2418+
],
2419+
}
2420+
2421+
result = setup_environment.create_additional_settings(
2422+
hooks,
2423+
claude_dir,
2424+
'test',
2425+
)
2426+
2427+
assert result is True
2428+
settings_file = claude_dir / 'test-additional-settings.json'
2429+
settings = json.loads(settings_file.read_text())
2430+
2431+
# Verify command does NOT include config path
2432+
hook_cmd = settings['hooks']['PostToolUse'][0]['hooks'][0]['command']
2433+
assert 'test.py' in hook_cmd
2434+
# Command should end with the Python file, not a config
2435+
assert hook_cmd.endswith('test.py')
2436+
2437+
def test_create_additional_settings_hook_config_with_query_params(self) -> None:
2438+
"""Test hook config with query parameters in filename."""
2439+
with tempfile.TemporaryDirectory() as tmpdir:
2440+
claude_dir = Path(tmpdir)
2441+
hooks_dir = claude_dir / 'hooks'
2442+
hooks_dir.mkdir(parents=True, exist_ok=True)
2443+
2444+
hook_file = hooks_dir / 'hook.py'
2445+
hook_file.write_text('print("hook")')
2446+
config_file = hooks_dir / 'config.yaml'
2447+
config_file.write_text('key: value')
2448+
2449+
hooks = {
2450+
'files': [
2451+
'hooks/hook.py?token=abc123',
2452+
'configs/config.yaml?token=abc123',
2453+
],
2454+
'events': [
2455+
{
2456+
'event': 'PreToolUse',
2457+
'matcher': '',
2458+
'type': 'command',
2459+
'command': 'hook.py',
2460+
'config': 'config.yaml?token=abc123',
2461+
},
2462+
],
2463+
}
2464+
2465+
result = setup_environment.create_additional_settings(
2466+
hooks,
2467+
claude_dir,
2468+
'test',
2469+
)
2470+
2471+
assert result is True
2472+
settings_file = claude_dir / 'test-additional-settings.json'
2473+
settings = json.loads(settings_file.read_text())
2474+
2475+
# Verify query params are stripped from config path
2476+
hook_cmd = settings['hooks']['PreToolUse'][0]['hooks'][0]['command']
2477+
assert 'config.yaml' in hook_cmd
2478+
assert '?token=' not in hook_cmd
2479+
2480+
def test_create_additional_settings_non_python_hook_with_config(self) -> None:
2481+
"""Test non-Python hook with config file."""
2482+
with tempfile.TemporaryDirectory() as tmpdir:
2483+
claude_dir = Path(tmpdir)
2484+
hooks_dir = claude_dir / 'hooks'
2485+
hooks_dir.mkdir(parents=True, exist_ok=True)
2486+
2487+
hook_file = hooks_dir / 'hook.sh'
2488+
hook_file.write_text('#!/bin/bash\necho "hook"')
2489+
config_file = hooks_dir / 'config.yaml'
2490+
config_file.write_text('key: value')
2491+
2492+
hooks = {
2493+
'files': ['hooks/hook.sh', 'configs/config.yaml'],
2494+
'events': [
2495+
{
2496+
'event': 'SessionStart',
2497+
'matcher': '',
2498+
'type': 'command',
2499+
'command': 'hook.sh',
2500+
'config': 'config.yaml',
2501+
},
2502+
],
2503+
}
2504+
2505+
result = setup_environment.create_additional_settings(
2506+
hooks,
2507+
claude_dir,
2508+
'test',
2509+
)
2510+
2511+
assert result is True
2512+
settings_file = claude_dir / 'test-additional-settings.json'
2513+
settings = json.loads(settings_file.read_text())
2514+
2515+
# Non-Python scripts should also get config appended
2516+
hook_cmd = settings['hooks']['SessionStart'][0]['hooks'][0]['command']
2517+
assert 'hook.sh' in hook_cmd
2518+
assert 'config.yaml' in hook_cmd

0 commit comments

Comments
 (0)