Files
owlen/PLUGIN_HOOKS_INTEGRATION.md

364 lines
9.5 KiB
Markdown

# 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