Back to Blog
Engineering

Everything Has Hooks - The Philosophy Behind Kollab CLI

Jan 2026-10 min read-Kollabor Team

Why we made every interaction in our terminal AI hookable and extensible. A deep dive into our plugin architecture.

The Philosophy of Hookable Everything

When we started building Kollab CLI, we had a simple but radical idea: what if everything was hookable? Every keystroke, every LLM request, every tool call, every render frame—exposed as an event that plugins could observe, modify, or cancel.

This wasn't just about extensibility. It was about creating a system where users could shape their AI experience without waiting for us to implement features. Where the community could build workflows we never imagined.

In this deep dive, we'll explore how we built this system, the architecture that powers it, and real examples of plugins that transform the CLI into something entirely new.

The Event Bus Architecture

At the heart of Kollab CLI sits the EventBus—a lightweight coordinator that manages hook registration and event processing. The system is built from three core components:

  • HookRegistry – Manages hook registration, organized by event type and priority
  • HookExecutor – Executes hooks with timeout handling, retry logic, and error isolation
  • EventProcessor – Coordinates sequential event processing through pre/main/post phases

Here's the EventBus initialization:

class EventBus:
    """Simplified event bus system for plugin communication."""

    """Coordinates between specialized components for hook registration"""
    """and event processing with clean separation of concerns."""

    # Specialized components
    hook_registry: HookRegistry
    hook_executor: HookExecutor
    event_processor: EventProcessor

    # Service lookup for cross-plugin communication
    _services: Dict[str, Any]

This separation of concerns means each component has a single responsibility. The registry tracks hooks, the executor runs them safely, and the processor orchestrates the flow.

Hook Registration and Organization

Hooks are organized by event type and priority. When a plugin registers a hook, it specifies:

  • event_type – Which event to listen for (from 50+ EventType values)
  • priority – Execution order (SYSTEM=1000, SECURITY=900, LLM=100, DISPLAY=10)
  • callback – Async function that receives event data
  • timeout – Maximum execution time before cancellation
  • error_action – "continue" or "stop" on failure

The Hook dataclass captures all of this:

@dataclass
class Hook:
    """Hook definition for the event system."""

    name: str
    plugin_name: str
    event_type: Union[EventType, str]
    priority: int
    callback: Callable
    enabled: bool = True
    timeout: Optional[int] = None
    retry_attempts: Optional[int] = None
    error_action: Optional[str] = None
    status: HookStatus = HookStatus.PENDING

50+ Event Types: Everything is Hookable

We defined over 50 event types across the entire application lifecycle. Here are the key categories:

User Input Events

USER_INPUT_PRE, USER_INPUT, USER_INPUT_POST
KEY_PRESS_PRE, KEY_PRESS, KEY_PRESS_POST
PASTE_DETECTED

LLM Events

LLM_REQUEST_PRE, LLM_REQUEST, LLM_REQUEST_POST
LLM_RESPONSE_PRE, LLM_RESPONSE, LLM_RESPONSE_POST
LLM_THINKING, CANCEL_REQUEST

Tool Events

TOOL_CALL_PRE, TOOL_CALL, TOOL_CALL_POST
PERMISSION_CHECK, PERMISSION_GRANTED
PERMISSION_DENIED

System Events

SYSTEM_STARTUP, SYSTEM_READY, SYSTEM_SHUTDOWN
RENDER_FRAME, INPUT_RENDER
COMMAND_MENU_SHOW, COMMAND_MENU_SELECT

Notice the pattern: events with _PRE and _POST suffixes allow plugins to intercept operations before and after they occur. This is how the permission system can intercept tool calls, or how a monitoring plugin can track LLM request times.

Priority-Based Execution

Hooks execute in priority order—higher numbers first. This ensures critical hooks run before optional ones:

class HookPriority(Enum):
    SYSTEM = 1000      # Core initialization, startup/shutdown
    SECURITY = 900     # Permission checks, security validation
    PREPROCESSING = 500 # Input transformation, validation
    LLM = 100         # LLM request/response handling
    POSTPROCESSING = 50 # Response transformation, logging
    DISPLAY = 10       # UI updates, rendering

For example, when a tool call occurs:

  1. SECURITY priority hooks run first (permission checks)
  2. PREPROCESSING hooks validate the command
  3. LLM hooks handle the execution
  4. POSTPROCESSING hooks log results
  5. DISPLAY hooks update the UI

Safe Hook Execution

A hook system is only useful if it can't crash the application. The HookExecutor wraps every hook in protective layers:

  • Timeout protection – Each hook has a configurable timeout (default 30s)
  • Retry logic – Failed hooks retry with exponential backoff
  • Error isolation – Exceptions are logged but don't crash the system
  • Thread safety – Per-hook locks prevent concurrent execution

Here's the execution loop:

# Retry loop with exponential backoff
max_attempts = retry_attempts + 1
for attempt in range(max_attempts):
    try:
        # Execute hook with timeout
        result = await asyncio.wait_for(
            hook.callback(event.data, event),
            timeout=timeout
        )
        # Success - break out of retry loop
        break

    except asyncio.TimeoutError:
        # Retry with exponential backoff
        backoff_delay = min(2**attempt, 30)
        await asyncio.sleep(backoff_delay)

Plugin Example: Hook Monitoring System

The HookMonitoringPlugin showcases the full power of the system. It's a comprehensive demonstration plugin that:

  • Monitors all 50+ event types for performance tracking
  • Discovers other plugins via PluginFactory
  • Registers monitoring services via KollaborPluginSDK
  • Provides health check services to other plugins
  • Displays real-time metrics in the status bar

Plugin initialization shows the full pattern:

class HookMonitoringPlugin:
    """Showcase: Demonstrates ALL plugin ecosystem features!"""

    """- Hook monitoring and performance tracking"""
    """- Plugin discovery via PluginFactory"""
    """- Service registration via KollaborPluginSDK"""
    """- Cross-plugin communication"""

    """Perfect for developers learning the plugin system!"""

    # State: performance tracking, plugin discovery, metrics
    hook_executions: int = 0
    hook_performance: Dict = {}
    discovered_plugins: Dict = {}
    registered_services: List = []

    # Hook: Track all event executions
    async def _log_hook_execution(
        self, data: Dict[str, Any], event: Event
    ) -> Dict[str, Any]:
        """Log hook execution with performance tracking."""
        self.hook_executions += 1
        self._update_performance_metrics(event.type.value)
        return {
            "status": "monitored",
            "execution_count": self.hook_executions,
        }

The plugin registers hooks across all event types:

# User input events
Hook(
    name="monitor_user_input_pre",
    plugin_name=self.name,
    event_type=EventType.USER_INPUT_PRE,
    priority=HookPriority.POSTPROCESSING.value,
    callback=self._log_hook_execution,
    timeout=5,
)

# LLM events
Hook(
    name="monitor_llm_request",
    plugin_name=self.name,
    event_type=EventType.LLM_REQUEST,
    priority=HookPriority.LLM.value,
    callback=self._log_hook_execution,
)

# Tool events
Hook(
    name="monitor_tool_call",
    plugin_name=self.name,
    event_type=EventType.TOOL_CALL,
    priority=HookPriority.LLM.value,
    callback=self._log_hook_execution,
)

The LLM Hook System

The LLMHookSystem in core demonstrates how the core services use hooks internally. It registers hooks for:

  • Pre-processing – Enrich user input with session context
  • Request handling – Track LLM API calls
  • Tool calls – Validate commands before execution
  • Post-processing – Analyze response quality
  • System lifecycle – Initialize and cleanup conversations

Each hook returns modified data that flows to the next handler:

async def _handle_pre_user_input(
    self, data: Dict[str, Any], event
) -> Dict[str, Any]:
    """Handle pre-user-input events with context enrichment."""
    message = data.get("message", "")

    # Enrich with session context
    return {
        "original_message": message,
        "message": message,
        "session_context": self._get_session_context(),
    }

Cross-Plugin Communication

Hooks aren't just for reacting to events—they're how plugins communicate. The monitoring plugin demonstrates this by registering services that other plugins can call:

# Register monitoring services
sdk.register_custom_tool({
    "name": "monitor_performance",
    "description": "Monitor plugin performance",
    "handler": self._provide_performance_monitoring,
    "category": "monitoring",
})

sdk.register_custom_tool({
    "name": "check_plugin_health",
    "description": "Check health status of any plugin",
    "handler": self._provide_health_check,
    "category": "health",
})

Any plugin can then call these services through the SDK:

# From another plugin
health = await sdk.execute_custom_tool(
    "check_plugin_health",
    {"plugin_name": "system", "detailed": True}
)

Building Your Own Plugin

Creating a plugin is straightforward. Here's the minimal structure:

from core.events import EventType, Hook, HookPriority

class MyPlugin:
    """A simple plugin that hooks into user input."""

    def __init__(self, name, event_bus, renderer, config):
        self.name = name
        self.event_bus = event_bus
        self.renderer = renderer
        self.config = config

    async def initialize(self):
        """Called when plugin loads."""
        pass

    async def register_hooks(self):
        """Register your hooks here."""
        await self.event_bus.register_hook(
            Hook(
                name="my_hook",
                plugin_name=self.name,
                event_type=EventType.USER_INPUT_PRE,
                priority=HookPriority.PREPROCESSING.value,
                callback=self._my_handler,
            )
        )

    async def _my_handler(self, data, event):
        """Your hook logic here."""
        # Modify data, log, or trigger actions
        return data

    async def shutdown(self):
        """Cleanup when plugin unloads."""
        pass

Drop this in the plugins/ directory, and Kollab CLI will discover and load it automatically.

Why This Matters

A hookable architecture transforms a tool into a platform. Instead of waiting for features, users can:

  • Add custom slash commands for domain-specific workflows
  • Transform LLM responses to match their team's format
  • Integrate with external tools and services
  • Build custom visualizations and dashboards
  • Enforce security policies at the hook level

We've already seen plugins we never imagined—everything from project-specific workflow automation to custom response formatters. The community builds features we wouldn't have thought of, and they do it faster than we could.

That's the power of "everything has hooks."

Get Started

Ready to build your own plugin? Check out our plugin documentation for detailed guides, or explore the HookMonitoringPlugin source code for a complete example.

Join us on GitHub to share your creations with the community.

Share this post