Comparison: HookRegistry vs HookConfig vs direct HookExecutor

Overview

The hooks system gives you three distinct entry points for responding to Claude Code lifecycle events. Each suits a different integration style. This page helps you choose the right one.

Capability HookRegistry HookConfig (YAML) HookExecutor directly
Registration style Programmatic, at runtime Declarative, from a YAML file Manual — you supply the HookDefinition
Conditional matching HookMatcher per handler HookMatcher in YAML rules You evaluate conditions yourself
Priority ordering priority param on register() priority field in YAML You control execution order
Async execution fire() Loaded via load_config(), then fire() HookExecutor.execute()
Sync execution fire_sync() Loaded via load_config(), then fire_sync() HookExecutorSync.execute()
Execution logging get_execution_log(), get_stats() Same, once config is loaded into a registry None built-in
Portable config Not portable without code from_yaml() / to_yaml() round-trips Not applicable
Handler type Python Callable Python handlers injected at executor init Python Callable in python_handlers dict
Best for Long-lived services, dynamic registration Shareable, file-driven configuration One-off execution or testing a single hook

Option details

HookRegistry — runtime dispatch hub

HookRegistry is the highest-level interface. It owns the full lifecycle: registration, matching, dispatch, and observability.

from hooks import HookRegistry, HookEvent

registry = HookRegistry()
handler_id = registry.register(
    HookEvent.PRE_TOOL,
    my_handler,
    description="Validate before tool runs",
    priority=10,
)

results = registry.fire(HookEvent.PRE_TOOL, context={"tool": "bash"})
registry.get_execution_log(limit=50, event_filter=HookEvent.PRE_TOOL)

Key characteristics:

HookConfig — declarative, file-driven configuration

HookConfig is the right choice when you want hook definitions to live outside Python code. Load from or save to YAML with from_yaml() and to_yaml().

from hooks import HookConfig, HookEvent

config = HookConfig.from_yaml(".attune/hooks.yaml")
rules = config.get_hooks_for_event(HookEvent.POST_TOOL)

# Or build programmatically and persist
config.add_hook(HookEvent.PRE_TOOL, my_definition, matcher=my_matcher, priority=5)
config.to_yaml(".attune/hooks.yaml")

A HookConfig on its own does not fire hooks — you load it into a HookRegistry via registry.load_config(config) to get dispatch and logging.

HookExecutor / HookExecutorSync — single-hook execution

HookExecutor and its synchronous counterpart HookExecutorSync sit at the bottom of the stack. Both expose a single execute(hook, context) method. Use them when you have a HookDefinition in hand and want to run it without setting up a full registry.

from hooks.executor import HookExecutor, HookExecutorSync

executor = HookExecutor(python_handlers={"my_fn": my_callable})
result = executor.execute(hook_definition, context)

# In a synchronous context:
sync_executor = HookExecutorSync(python_handlers={"my_fn": my_callable})
result = sync_executor.execute(hook_definition, context)

There is no built-in logging or matching at this level — you trade observability for simplicity.

Tradeoffs summary

Concern Winner Notes
Simplest setup HookExecutor / HookExecutorSync No registry, no config — just execute()
Best observability HookRegistry get_execution_log(), get_stats(), event_filter
Shareable config HookConfig + YAML from_yaml() / to_yaml() make config portable across environments
Dynamic handlers HookRegistry register() / unregister() at runtime without touching files
Sync-only environments HookExecutorSync or fire_sync() Both paths support synchronous execution

Use X when...

Use HookRegistry when you are building a long-lived service or agent integration that needs to register and unregister handlers dynamically, inspect execution history via get_execution_log(), or fire hooks across multiple event types with priority ordering.

Use HookConfig + from_yaml() when hook definitions should be version-controlled, shared across environments, or edited without deploying Python code. Load the config into a HookRegistry to get full dispatch and logging on top of it.

Use HookExecutor or HookExecutorSync when you need to run a single known HookDefinition in isolation — for example, in a test, a one-off migration script, or a context where standing up a registry is more overhead than the task warrants.

Avoid wiring up any of these for purely exploratory work or throwaway scripts. The built-in script functions (run_evaluate_session(), run_pre_compact(), check_init()) already handle the most common lifecycle tasks without requiring you to configure hooks manually.

Tags: hooks, webhooks, events, automation

Unresolved references

Auto-generated by attune-author fact-check. Review and either fix the source code, fix this doc, or add an override.

Location Severity Issue
Line 35 (code fence) error from hooks import … — module not importable
Line 77 (code fence) error from hooks.executor import … — module not importable