The hook system — register handlers for lifecycle events, fire them in-process, or drive them from config

Tasks

Register and fire an in-process hook

from attune.hooks import HookRegistry, HookEvent

registry = HookRegistry()


def guard(**context) -> dict:
    return {"blocked": context.get("tool_name") == "Bash"}


hook_id = registry.register(HookEvent.PRE_TOOL_USE, guard, priority=10)
results = registry.fire_sync(HookEvent.PRE_TOOL_USE, {"tool_name": "Bash"})
print(hook_id, results[0]["success"], results[0]["output"])

Verify: register(...) returns a hook id (a str). fire_sync runs every matching handler — calling each as handler(**context) — and returns a list of result dicts (a success record carries event, hook, description, success, output, error, duration_ms; an error record is a subset). fire(...) is the async variant.

Load hooks from YAML config

Goal: declare hooks in a file instead of code.

Steps: HookConfig.from_yaml(path) returns a HookConfig; get_hooks_for_event(event) lists the HookRules for an event. Each rule's hooks are HookDefinitions an executor can run.

from attune.hooks import HookConfig, HookEvent

config = HookConfig.from_yaml("hooks.yaml")
for rule in config.get_hooks_for_event(HookEvent.PRE_TOOL_USE):
    print(rule.description, rule.priority)

Verify: from_yaml is a constructor returning HookConfig; get_hooks_for_event returns list[HookRule].

Execute a configured hook

import asyncio

from attune.hooks import HookExecutor, HookDefinition
from attune.hooks.config import HookType

hook = HookDefinition(type=HookType.COMMAND, command="echo hi", timeout=5)
executor = HookExecutor()
result = asyncio.run(executor.execute(hook, {"tool_name": "Bash"}))
print(result)

Verify: HookExecutor.execute(hook, context) is async — await it; it returns a result dict.