# Plugin Hooks Integration This document describes how the plugin system integrates with the hook system to allow plugins to define lifecycle hooks. ## Overview Plugins can now define hooks in a `hooks/hooks.json` file that will be automatically registered with the `HookManager` during application startup. This allows plugins to: - Intercept and validate tool calls before execution (PreToolUse) - React to tool execution results (PostToolUse) - Run code at session boundaries (SessionStart, SessionEnd) - Process user input (UserPromptSubmit) - Handle context compaction (PreCompact) ## Plugin Hook Configuration Plugins define hooks in `hooks/hooks.json`: ```json { "description": "Validation and logging hooks for the plugin", "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validate.py", "timeout": 5000 } ] }, { "matcher": "Bash", "hooks": [ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/bash_guard.sh" } ] } ], "PostToolUse": [ { "hooks": [ { "type": "command", "command": "echo 'Tool executed' >> ${CLAUDE_PLUGIN_ROOT}/logs/tool.log && exit 0" } ] } ] } } ``` ### Hook Configuration Schema - **description** (optional): A human-readable description of what the hooks do - **hooks**: A map of event names to hook matchers - **PreToolUse**: Hooks that run before a tool is executed - **PostToolUse**: Hooks that run after a tool is executed - **SessionStart**: Hooks that run when a session starts - **SessionEnd**: Hooks that run when a session ends - **UserPromptSubmit**: Hooks that run when the user submits a prompt - **PreCompact**: Hooks that run before context compaction ### Hook Matcher Each hook matcher contains: - **matcher** (optional): A regex pattern to match against tool names (for PreToolUse events) - Example: `"Edit|Write"` matches both Edit and Write tools - Example: `".*"` matches all tools - If not specified, the hook applies to all tools - **hooks**: An array of hook definitions ### Hook Definition Each hook definition contains: - **type**: The hook type (`"command"` or `"prompt"`) - **command**: The shell command to execute (for command-type hooks) - Can use `${CLAUDE_PLUGIN_ROOT}` which is replaced with the plugin's base path - **prompt** (future): An LLM prompt for AI-based validation - **timeout** (optional): Timeout in milliseconds (default: no timeout) ## Variable Substitution The following variables are automatically substituted in hook commands: - **${CLAUDE_PLUGIN_ROOT}**: The absolute path to the plugin directory - Example: `~/.config/owlen/plugins/my-plugin` - Useful for referencing scripts within the plugin ## Hook Execution Behavior ### Exit Codes Hooks communicate their decision via exit codes: - **0**: Allow the operation to proceed - **2**: Deny the operation (blocks the tool call) - **Other**: Error (operation fails with error message) ### Input/Output Hooks receive JSON input via stdin containing the event data: ```json { "event": "preToolUse", "tool": "Edit", "args": { "path": "/path/to/file.txt", "old_string": "foo", "new_string": "bar" } } ``` ### Pattern Matching For PreToolUse hooks, the `matcher` field is treated as a regex pattern: - `"Edit|Write"` - Matches Edit OR Write tools - `"Bash"` - Matches only Bash tool - `".*"` - Matches all tools - No matcher - Applies to all tools ### Multiple Hooks - Multiple plugins can define hooks for the same event - All matching hooks are executed in sequence - If any hook denies (exit code 2), the operation is blocked - File-based hooks in `.owlen/hooks/` are executed first, then plugin hooks ## Integration Architecture ### Loading Process 1. **Application Startup** (`main.rs`): ```rust // Create hook manager let mut hook_mgr = HookManager::new("."); // Register plugin hooks for plugin in app_context.plugin_manager.plugins() { if let Ok(Some(hooks_config)) = plugin.load_hooks_config() { for (event, command, pattern, timeout) in plugin.register_hooks_with_manager(&hooks_config) { hook_mgr.register_hook(event, command, pattern, timeout); } } } ``` 2. **Plugin Hook Loading** (`plugins/src/lib.rs`): - `Plugin::load_hooks_config()` reads and parses `hooks/hooks.json` - `Plugin::register_hooks_with_manager()` processes the config and performs variable substitution 3. **Hook Registration** (`hooks/src/lib.rs`): - `HookManager::register_hook()` stores hooks internally - `HookManager::execute()` filters and executes matching hooks ### Execution Flow ``` Tool Call Request ↓ Permission Check ↓ HookManager::execute(PreToolUse) ↓ Check file-based hook (.owlen/hooks/PreToolUse) ↓ Filter plugin hooks by event and pattern ↓ Execute each matching hook ↓ If any hook denies → Block operation ↓ If all allow → Execute tool ↓ HookManager::execute(PostToolUse) ``` ## Example: Validation Hook Create a plugin with a validation hook: **Directory structure:** ``` ~/.config/owlen/plugins/validation/ ├── plugin.json └── hooks/ ├── hooks.json └── validate.py ``` **plugin.json:** ```json { "name": "validation", "version": "1.0.0", "description": "Validation hooks for file operations" } ``` **hooks/hooks.json:** ```json { "description": "Validate file operations", "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validate.py", "timeout": 5000 } ] } ] } } ``` **hooks/validate.py:** ```python #!/usr/bin/env python3 import json import sys # Read event from stdin event = json.load(sys.stdin) tool = event.get('tool') args = event.get('args', {}) path = args.get('path', '') # Deny operations on system files if path.startswith('/etc/') or path.startswith('/sys/'): print(f"Blocked: Cannot modify system file {path}", file=sys.stderr) sys.exit(2) # Deny # Allow all other operations sys.exit(0) # Allow ``` **Make executable:** ```bash chmod +x ~/.config/owlen/plugins/validation/hooks/validate.py ``` ## Testing ### Unit Tests Test hook registration and execution: ```rust #[tokio::test] async fn test_plugin_hooks() -> Result<()> { let mut hook_mgr = HookManager::new("."); hook_mgr.register_hook( "PreToolUse".to_string(), "echo 'validated' && exit 0".to_string(), Some("Edit|Write".to_string()), Some(5000), ); let event = HookEvent::PreToolUse { tool: "Edit".to_string(), args: serde_json::json!({}), }; let result = hook_mgr.execute(&event, Some(5000)).await?; assert_eq!(result, HookResult::Allow); Ok(()) } ``` ### Integration Tests Test the full plugin loading and hook execution: ```rust #[tokio::test] async fn test_plugin_hooks_integration() -> Result<()> { // Create plugin with hooks let plugin_dir = create_test_plugin_with_hooks()?; // Load plugin let mut plugin_manager = PluginManager::with_dirs(vec![plugin_dir]); plugin_manager.load_all()?; // Register hooks let mut hook_mgr = HookManager::new("."); for plugin in plugin_manager.plugins() { if let Ok(Some(config)) = plugin.load_hooks_config() { for (event, cmd, pattern, timeout) in plugin.register_hooks_with_manager(&config) { hook_mgr.register_hook(event, cmd, pattern, timeout); } } } // Test hook execution let event = HookEvent::PreToolUse { tool: "Edit".to_string(), args: serde_json::json!({}), }; let result = hook_mgr.execute(&event, Some(5000)).await?; assert_eq!(result, HookResult::Allow); Ok(()) } ``` ## Implementation Details ### Modified Crates 1. **plugins** (`crates/platform/plugins/src/lib.rs`): - Added `PluginHooksConfig`, `HookMatcher`, `HookDefinition` structs - Added `Plugin::load_hooks_config()` method - Added `Plugin::register_hooks_with_manager()` method 2. **hooks** (`crates/platform/hooks/src/lib.rs`): - Refactored to store registered hooks internally - Added `HookManager::register_hook()` method - Updated `HookManager::execute()` to handle both file-based and registered hooks - Added pattern matching support using regex - Added `regex` dependency 3. **owlen** (`crates/app/cli/src/main.rs`): - Integrated plugin hook loading during startup - Registered plugin hooks with HookManager ### Dependencies Added - **hooks/Cargo.toml**: Added `regex = "1.10"` ## Benefits 1. **Modularity**: Hooks can be packaged with plugins and distributed independently 2. **Reusability**: Plugins can be shared across projects 3. **Flexibility**: Each plugin can define multiple hooks with different patterns 4. **Compatibility**: Works alongside existing file-based hooks in `.owlen/hooks/` 5. **Variable Substitution**: `${CLAUDE_PLUGIN_ROOT}` makes scripts portable ## Future Enhancements 1. **Prompt-based hooks**: Use LLM for validation instead of shell commands 2. **Hook priorities**: Control execution order of hooks 3. **Hook metadata**: Description, author, version for each hook 4. **Hook debugging**: Better error messages and logging 5. **Async hooks**: Support for long-running hooks that don't block