9.5 KiB
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:
{
"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
- Example:
- 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
- Can use
- 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
- Example:
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:
{
"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
-
Application Startup (
main.rs):// 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); } } } -
Plugin Hook Loading (
plugins/src/lib.rs):Plugin::load_hooks_config()reads and parseshooks/hooks.jsonPlugin::register_hooks_with_manager()processes the config and performs variable substitution
-
Hook Registration (
hooks/src/lib.rs):HookManager::register_hook()stores hooks internallyHookManager::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:
{
"name": "validation",
"version": "1.0.0",
"description": "Validation hooks for file operations"
}
hooks/hooks.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:
#!/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:
chmod +x ~/.config/owlen/plugins/validation/hooks/validate.py
Testing
Unit Tests
Test hook registration and execution:
#[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:
#[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
-
plugins (
crates/platform/plugins/src/lib.rs):- Added
PluginHooksConfig,HookMatcher,HookDefinitionstructs - Added
Plugin::load_hooks_config()method - Added
Plugin::register_hooks_with_manager()method
- Added
-
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
regexdependency
-
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
- Modularity: Hooks can be packaged with plugins and distributed independently
- Reusability: Plugins can be shared across projects
- Flexibility: Each plugin can define multiple hooks with different patterns
- Compatibility: Works alongside existing file-based hooks in
.owlen/hooks/ - Variable Substitution:
${CLAUDE_PLUGIN_ROOT}makes scripts portable
Future Enhancements
- Prompt-based hooks: Use LLM for validation instead of shell commands
- Hook priorities: Control execution order of hooks
- Hook metadata: Description, author, version for each hook
- Hook debugging: Better error messages and logging
- Async hooks: Support for long-running hooks that don't block