Home
Home/s04
s0490 lines of code

Hook System

Hang on the Loop, Don't Write into It

Key Insight:

Cross-cutting behavior belongs around the loop, not tangled inside it.

Hooks are lifecycle callbacks that plugins register at specific points in the agent's execution: before and after tool execution, when messages are created, during configuration loading, and more. This keeps the core loop clean while enabling rich extensibility. A slow hook never blocks the agent. Hooks run with timeouts, and any failures are logged without disrupting the main flow.

Architecture Flow

The Problem: Cross-Cutting Concerns Without Core Pollution

Logging, telemetry, custom validation, and audit trails are cross-cutting concerns that do not belong inside the agent loop or individual tool handlers. Without hooks, developers face an unpleasant choice: either litter the core with optional callbacks, making the logic harder to reason about, or fork the codebase, which makes upgrades impossible. Hooks solve this problem by defining extension points around the core loop. Any number of plugins can register for the same hook and execute in registration order. The core loop never imports plugin code. It simply checks whether any hooks are registered for a given event name.

Hook Design: Named Events with Typed Payloads

Each hook is a named event with a typed context object. 'tool.execute.before' receives { tool, input, opts }, and 'tool.execute.after' receives { tool, result, duration }. This structured approach makes hooks discoverable (event names follow a consistent convention), type-safe (each event defines its payload shape), and isolated (a single hook failure does not crash others, as timeouts are enforced). The naming convention, domain.event, mirrors DOM events and makes each hook's intent clear. 'tool.execute.before' runs before tool execution; 'config' runs when configuration loads.

Design Decisions

Hooks execute with configurable timeouts. A misbehaving plugin, whether stuck in an infinite loop or waiting on a slow network call, gets terminated after the timeout expires. The error is logged, and the core loop continues without interruption.

Hook registration order matters. Plugins registered first have their hooks executed first. This enables priority-based behavior: a security plugin can validate input before a logging plugin records it.

Comparison: Claude Code

Claude Code does not expose a plugin system in the traditional sense. OpenCode's lifecycle hooks (beforeToolExecute, afterToolExecute, beforeLlmCall, onConfigLoad) are a distinguishing feature. They allow any external code to intercept and augment the core loop without modifying the source. This is closer to VS Code's extension model than anything found in Claude Code.

Deep Dive: Design Decisions

Hooks over Inheritance or Middleware

Plugins register hooks at specific lifecycle points (beforeTool, afterTool, onMessage, etc.) rather than subclassing the agent. This keeps the core loop intact and makes plugin behavior composable.

Alternatives: Middleware (express-style) or subclassing could work, but hooks provide clearer lifecycle semantics and prevent middleware ordering bugs.

Hooks Must Be Non-Blocking by Default

A slow or broken hook should not block the agent. Each hook runs with a timeout, and failures are logged rather than propagated. The agent continues even if a hook errors.

Alternatives: Hard-fail on hook error would be simpler but makes the system fragile. Soft-fail with logging is more production-robust.

Learn OpenCode — Built with Next.js