364 lines
9.5 KiB
Markdown
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
|