Files
owlen/PLUGIN_HOOKS_INTEGRATION.md

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
  • 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:

{
  "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):

    // 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:

{
  "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

  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