Hook System
The Hook System
Every significant system has a question it must answer honestly: what happens when something you didn’t anticipate needs to happen?
Most systems answer this question with modification — you change the code, rebuild, redeploy. Obsidian answers it with hooks. The hook system is the event-driven backbone that connects plugins to the system’s lifecycle — every task completion, every agent spawn, every deployment, every configuration change emits events that plugins can intercept, observe, and transform.
This is Constitution Principle 11 made operational: extend without modifying core. Hooks are the mechanism by which that principle becomes architecture rather than aspiration.
Architecture
┌────────────────────────────────────────────────────────┐
│ OBSIDIAN Core │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Hook Registry │ │
│ │ ┌────────────┐ ┌────────────┐ ┌─────────────┐ │ │
│ │ │ Register │ │ Priority │ │ Lifecycle │ │ │
│ │ │ Handlers │ │ Ordering │ │ Management │ │ │
│ │ └────────────┘ └────────────┘ └─────────────┘ │ │
│ └──────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌──────────────────────▼───────────────────────────┐ │
│ │ Event Dispatcher │ │
│ │ │ │
│ │ agent:* │ task:* │ deploy:* │ config:* │ │
│ │ file:* │ secret:*│ plugin:* │ system:* │ │
│ └──────┬────────┬────────┬────────┬────────────────┘ │
│ │ │ │ │ │
│ ┌──────▼──┐ ┌───▼────┐ ┌▼──────┐ ┌▼───────┐ │
│ │Plugin A │ │Plugin B│ │Plugin C│ │Plugin D│ │
│ │priority │ │priority│ │priority│ │priority│ │
│ │ 10 │ │ 50 │ │ 50 │ │ 90 │ │
│ └─────────┘ └────────┘ └───────┘ └────────┘ │
└────────────────────────────────────────────────────────┘
The hook system has three components that matter:
- Hook Registry — tracks which handlers are registered for which events, maintains priority ordering, handles lifecycle (registration, deregistration, cleanup)
- Event Dispatcher — routes emitted events to registered handlers in priority order, manages execution timeouts, and handles failures gracefully
- Event Namespaces — a hierarchical naming convention (
namespace:action) that allows both specific subscriptions and wildcard matching
Hook Types
Obsidian defines eight hook namespaces, each corresponding to a domain of system activity. This is not arbitrary taxonomy — each namespace maps to a real operational boundary where external code might legitimately need to intervene.
Agent Hooks
Events emitted during the Agent lifecycle. Every state transition — spawn, claim, complete, fail — fires a hook.
| Event | Fires When | Payload |
|---|---|---|
agent:spawned | New agent process starts | Agent ID, config, role |
agent:claimed | Agent claims a task | Agent ID, task ID |
agent:completed | Agent finishes work | Agent ID, result, metrics |
agent:failed | Agent encounters unrecoverable error | Agent ID, error, retry count |
agent:recycled | Agent is cleaned up and returned to pool | Agent ID, lifetime stats |
Task Hooks
Events in the task lifecycle — from creation through completion or failure.
| Event | Fires When | Payload |
|---|---|---|
task:created | New task enters the queue | Task ID, priority, source |
task:assigned | Task is assigned to an agent | Task ID, agent ID |
task:completed | Task finishes successfully | Task ID, result, duration |
task:failed | Task fails after retries exhausted | Task ID, error, attempts |
task:escalated | Task is escalated to a human | Task ID, reason, context |
Deploy Hooks
Deployment lifecycle events — the critical path where hooks can gate, audit, or transform deployments.
| Event | Fires When | Payload |
|---|---|---|
deploy:started | Deployment begins | Deploy ID, target, version |
deploy:validated | Pre-deploy checks pass | Deploy ID, check results |
deploy:completed | Deployment succeeds | Deploy ID, duration, URL |
deploy:rolled-back | Rollback triggered | Deploy ID, reason, previous version |
deploy:failed | Deployment fails | Deploy ID, error, stage |
Config Hooks
Configuration changes — because in a system governed by a Constitution , configuration changes are governance events.
| Event | Fires When | Payload |
|---|---|---|
config:changed | Any configuration value changes | Key, old value, new value |
config:validated | Configuration passes validation | Full config snapshot |
config:reloaded | Hot-reload of configuration | Changed keys, source |
File Hooks
Filesystem events within the project workspace.
| Event | Fires When | Payload |
|---|---|---|
file:created | New file written | Path, size, creator |
file:modified | Existing file changed | Path, diff summary |
file:deleted | File removed | Path, last modifier |
Secret Hooks
Secret access events — every secret read is auditable, every rotation is observable.
| Event | Fires When | Payload |
|---|---|---|
secret:accessed | Secret value read | Key pattern, accessor, timestamp |
secret:rotated | Secret value changed | Key pattern, rotation source |
secret:expired | Secret TTL elapsed | Key pattern, expiry time |
Plugin Hooks
Meta-hooks — events about the plugin system itself.
| Event | Fires When | Payload |
|---|---|---|
plugin:loaded | Plugin successfully loads | Plugin name, version, type |
plugin:enabled | Plugin activated | Plugin name, hooks registered |
plugin:disabled | Plugin deactivated | Plugin name, reason |
plugin:error | Plugin throws unhandled error | Plugin name, error, context |
System Hooks
System-level events — startup, shutdown, health changes.
| Event | Fires When | Payload |
|---|---|---|
system:startup | Obsidian process starts | Version, config, environment |
system:shutdown | Graceful shutdown initiated | Reason, active tasks count |
system:health-changed | Health status transitions | Previous state, new state, cause |
Hook Registration
Hooks are registered through the plugin manifest or programmatically via the Plugin API. Both paths converge on the same registry — the manifest approach is declarative and preferred; the programmatic approach exists for dynamic registration patterns.
Manifest Registration
The static approach. Declare hooks in obsidian-plugin.json and they are registered when the plugin loads:
{
"hooks": [
{
"event": "task:completed",
"handler": "onTaskCompleted",
"priority": 50
},
{
"event": "deploy:started",
"handler": "onDeployStarted",
"priority": 10
},
{
"event": "agent:*",
"handler": "onAnyAgentEvent",
"priority": 90
}
]
}
The wildcard pattern (agent:*) subscribes to every event in the namespace. This is useful for monitoring and audit plugins that need comprehensive visibility without knowing every specific event type in advance.
Programmatic Registration
The dynamic approach. Use the Plugin API when registration depends on runtime conditions:
export function activate(api: ObsidianAPI) {
// Register a specific hook
api.hooks.on('task:completed', async (event) => {
await notifySlack(event.payload.taskId, event.payload.result);
}, { priority: 50 });
// Register a wildcard hook
api.hooks.on('deploy:*', async (event) => {
await auditLog.record(event);
}, { priority: 10 });
// Conditional registration
if (api.config.get('features.codeReview')) {
api.hooks.on('file:modified', async (event) => {
if (event.payload.path.endsWith('.rs')) {
await triggerCodeReview(event.payload);
}
}, { priority: 50 });
}
}
Execution Model
Hook execution follows rules that prioritize system stability over plugin convenience. This is a deliberate tradeoff — a hook system that can crash the host is worse than no hook system at all.
Priority Ordering
Handlers execute in priority order — lower numbers run first. The scale is 0–100:
- 0–20: System-critical hooks (audit, security, compliance)
- 21–50: Standard processing hooks (notifications, transformations)
- 51–80: Enhancement hooks (analytics, logging, telemetry)
- 81–100: Cleanup hooks (resource release, cache invalidation)
Handlers at the same priority level execute in registration order. This is deterministic — the same set of plugins will always execute in the same order.
Timeout Enforcement
Every hook handler has a maximum execution time, configurable per-hook and globally:
hooks:
default_timeout_ms: 5000
max_timeout_ms: 30000
overrides:
"deploy:*": 30000
"task:completed": 10000
A handler that exceeds its timeout is killed and logged. The event continues dispatching to remaining handlers. A single slow plugin does not block the system. This is Constitution Principle 1 in miniature: failures are inevitable; design for survival.
Error Isolation
When a hook handler throws an error:
- The error is caught and logged with full context (plugin name, event, payload, stack trace)
- The handler is marked as failed for this invocation
- Remaining handlers continue executing
- The originating operation is not affected — a failed hook never crashes the operation that triggered it
If a handler fails repeatedly (configurable threshold, default: 5 failures in 10 minutes), the hook registry automatically disables it and emits a plugin:error event. The Warden can then decide whether to restart the plugin or escalate.
Synchronous vs. Asynchronous
Hooks execute asynchronously by default — the operation that emits the event does not wait for handlers to complete. This is the correct default because most hooks are observational (logging, metrics, notifications).
For hooks that must gate an operation — pre-deploy validation, for instance — handlers can be registered as synchronous:
{
"event": "deploy:started",
"handler": "validateDeploy",
"priority": 5,
"sync": true
}
Synchronous handlers can return a result that affects the operation. A deploy:started sync handler that returns { abort: true, reason: "..." } will cancel the deployment. This is power with accountability — sync hooks are logged more aggressively and have stricter timeout enforcement.
The Hook Lifecycle
Operation (e.g., task completes)
│
▼
┌─────────────┐
│ Emit Event │──── event: "task:completed"
└──────┬──────┘ payload: { taskId, result, duration }
│
▼
┌─────────────┐
│ Registry │──── lookup handlers for "task:completed"
│ Lookup │ + handlers for "task:*"
└──────┬──────┘
│
▼
┌─────────────┐
│ Sort by │──── priority 10, 50, 50, 90
│ Priority │
└──────┬──────┘
│
▼
┌─────────────┐ ┌─────────┐
│ Execute │────▶│Handler 1│──── priority 10 (sync: gate)
│ Pipeline │ └────┬────┘
│ │ ▼
│ │ ┌─────────┐
│ │────▶│Handler 2│──── priority 50 (async: notify)
│ │ └────┬────┘
│ │ ▼
│ │ ┌─────────┐
│ │────▶│Handler 3│──── priority 50 (async: log)
│ │ └────┬────┘
│ │ ▼
│ │ ┌─────────┐
│ │────▶│Handler 4│──── priority 90 (async: cleanup)
│ │ └─────────┘
└─────────────┘
Writing Hook Handlers
A hook handler is a function that receives an event object and optionally returns a result. The event object is standardized across all hook types:
interface HookEvent<T = unknown> {
/** The event name, e.g. "task:completed" */
name: string;
/** ISO 8601 timestamp of emission */
timestamp: string;
/** Unique event ID for correlation */
id: string;
/** The event-specific data */
payload: T;
/** Source of the event (system, plugin name, agent ID) */
source: string;
}
A practical example — a quality gate that blocks deployments unless all tests pass:
import type { ObsidianAPI, HookEvent } from '@obsidian/types';
interface DeployPayload {
deployId: string;
target: string;
version: string;
testResults?: { passed: number; failed: number };
}
export function activate(api: ObsidianAPI) {
api.hooks.on('deploy:started', async (event: HookEvent<DeployPayload>) => {
const { deployId, testResults } = event.payload;
if (!testResults) {
return { abort: true, reason: 'No test results found — refusing to deploy blind.' };
}
if (testResults.failed > 0) {
api.log.warn(`Deploy ${deployId} blocked: ${testResults.failed} failing tests`);
return { abort: true, reason: `${testResults.failed} tests failing.` };
}
api.log.info(`Deploy ${deployId} cleared: ${testResults.passed} tests passing`);
}, { priority: 5, sync: true });
}
Why This Matters
The hook system is where Constitution Principle 2 (if it isn’t observable, it doesn’t exist) and Principle 11 (extend without modifying core) converge. Every significant operation emits events. Every event can be observed. Every observation can trigger action. And none of this requires changing a single line of core code.
This is not a convenience feature. This is the architectural guarantee that Obsidian can grow in directions its creators never anticipated — because the extension points are not afterthoughts bolted onto specific features, but a universal event fabric woven through the entire system. The same pattern at every scale. Fractal Delegation applied to extensibility itself.