Skip to main content

What are hooks?

Hooks are one of Extra’s core extension mechanisms. They are Python code that the engine runs automatically at well-defined moments in the runtime lifecycle — when the engine starts, when a run begins or fails, and around every tool call and MCP request. They exist for the behavior that has to happen outside the LLM: deterministic integration and side-effect logic that the framework — not the model — controls. Typical uses:
  • Environment validation — fail fast at startup if a required credential or connection is missing
  • MCP authentication — inject auth headers into outgoing MCP requests
  • Auditing — record tool calls and run outcomes as safe metadata
  • Policy — reshape or cap a tool result before it reaches the model
  • Context enrichment — attach identity or metadata to a run
  • Error handling — record run failures for alerting or observability
  • Enterprise integration — bridge the engine to internal systems without changing the engine
The dividing line is simple: YAML declares the structure of the system. Hooks are code. The YAML only registers a hook and says where it runs; the behavior lives in Python.

What hooks are not

Hooks are not a place for business logic the model should decide to run, and not a place for secrets in YAML.
  • Hooks are not prompts. They contain no natural-language instructions.
  • Hooks are not tools. The model never sees a hook, cannot name it, and cannot call it. If you want the model to be able to invoke something, that is a tool, not a hook.
  • Hooks are not agent instructions. They do not steer the model; they run around it.
  • Hooks are not YAML business logic. YAML registers a hook — it does not configure its behavior. There is no per-hook config: block. If a hook needs a value, it reads it in Python.
  • Hooks are not a home for secrets. Never put tokens, keys, or credentials in YAML. Secrets live in environment variables; the hook reads them directly at runtime.
Hooks are deterministic runtime code controlled by the framework lifecycle — they run when the engine reaches a lifecycle point, every time, regardless of what the model does.

Hooks vs. tools


The lifecycle

Hooks run at fixed points as a request flows through the engine:
Every point below is a moment the engine will call your hook, if you have registered one for it. Unregistered points cost nothing.

Engine lifecycle

Fires once per engine process, not per request.

Run lifecycle

Fires once per request.

Tool lifecycle

Fires around every local or MCP tool call.

MCP lifecycle

Fires around every outgoing MCP HTTP request.
The context objects hooks receive carry safe metadata only. Tool arguments and full results are deliberately omitted from the observe-only points, because they can hold sensitive user data. transform_tool_result is the one point that sees a tool’s result text — precisely so it can reshape it — and is treated as trusted code that must never log the result body.

Declaring a hook

A hook has two parts: a registration in YAML, and the Python code it points to. agents.yaml — register the hook under its lifecycle point. Each point takes a list of entries, and each entry names a plugin and the method to call on it:
plugins/plugins.toml — map the plugin id to its Python class. This is the only place the import path lives:
The YAML never contains an import path, a config block, or a secret — it only says which hook runs where. agentctl generate creates and maintains the plugins.toml entry for you.

Field reference

string
Logical plugin id, resolved to a class through plugins/plugins.toml’s [hooks.plugins] table. Used together with method. Mutually exclusive with ref.
string
The method name to call on the resolved plugin class. Required when using plugin.
string
A direct module.path:function import to a hook function — an advanced alternative to plugin + method for manual wiring. Mutually exclusive with plugin + method.
string
default:"fail"
What happens if the hook itself raises.
There is intentionally no config: field. YAML registers a hook; it does not parameterize its behavior. A hook that needs values reads them from the environment (for secrets) or from the RunContext (for per-request identity) in Python.

The hook method signature

A managed hook method receives a single HookInvocation event and reads its typed payload with payload_as:
event.run_context gives per-request identity; event.payload_as(T) returns the point-specific context. Long-lived state (clients, caches) can live on the instance; per-request state must not — read it from the event each call.

MCP auth

The most common hook. YAML defines which MCP server exists; the hook injects authentication at runtime, so tokens never appear in YAML, prompts, or logs.
  • YAML defines the MCP server (see MCP & Tools).
  • Hook code injects the auth header before each request.
  • Secrets stay in environment variables; the hook reads them directly.
  • This scales to one header or many, bearer tokens, tenant-specific auth, HMAC signing, and full enterprise auth flows.
The token is read from the environment at request time. It never appears in YAML, prompts, or logs. Gate on request.server_id so public servers stay unauthenticated.

Token exchange

To exchange the caller’s inbound token for an MCP-scoped one:

HMAC signing


Auditing tool calls

Register audit hooks with failure_policy: warn — a logging failure shouldn’t abort the user’s request. Log only safe metadata: identifiers, provider, status, timing — never prompts, arguments, results, or secrets.

Reshaping tool results

Use transform_tool_result to cap or redact a result before it reaches the model. It must return a ToolResultContext:

Passing identity into hooks

The embedding application passes per-request identity through the run context:
Inside a hook, read it from the event:

Worked example: the Enterprise Knowledge Assistant

The enterprise-knowledge-assistant example ships a single hook plugin, ResearchHooksHook, that demonstrates the most useful lifecycle points end to end:
  • Environment validation (on_engine_start) — fails the build if ANTHROPIC_API_KEY or CONTEXT7_API_KEY is missing.
  • Authenticated MCP requests (before_mcp_request) — injects the Context7 key into context7 requests only; the public DeepWiki server stays unauthenticated.
  • Tool-call auditing (after_tool_call, failure_policy: warn) — logs safe metadata, never prompts, secrets, or results.
  • Result reshaping (transform_tool_result) — caps oversized DeepWiki responses before they reach the model.
  • Safe error handling (on_run_error) — records the failure’s type, not its message.
The code reads secrets from the environment and registers through plugins.toml — no secrets in YAML.

Security

Hooks are trusted in-process code. The engine never logs auth headers, tokens, HMAC signatures, or inbound access tokens. Your hook code must hold the same line:
  • Read secrets from environment variables, never from YAML.
  • Never log secrets, prompts, tool arguments, or raw tool results — log safe metadata only.
  • Prefer recording an error’s type in on_run_error, not its message, which may contain user input.
  • Keep security-critical hooks (like auth) at the default failure_policy: fail so they never fail open.