Everything Has Hooks - The Philosophy Behind Kollab CLI
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.PENDING50+ 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_DETECTEDLLM Events
LLM_REQUEST_PRE, LLM_REQUEST, LLM_REQUEST_POST
LLM_RESPONSE_PRE, LLM_RESPONSE, LLM_RESPONSE_POST
LLM_THINKING, CANCEL_REQUESTTool Events
TOOL_CALL_PRE, TOOL_CALL, TOOL_CALL_POST
PERMISSION_CHECK, PERMISSION_GRANTED
PERMISSION_DENIEDSystem 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, renderingFor example, when a tool call occurs:
- SECURITY priority hooks run first (permission checks)
- PREPROCESSING hooks validate the command
- LLM hooks handle the execution
- POSTPROCESSING hooks log results
- 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