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
What hooks are not
- 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 vs. tools
The lifecycle
Hooks run at fixed points as a request flows through the engine: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:
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 singleHookInvocation 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.
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
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
Usetransform_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:Worked example: the Enterprise Knowledge Assistant
Theenterprise-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 ifANTHROPIC_API_KEYorCONTEXT7_API_KEYis missing. - Authenticated MCP requests (
before_mcp_request) — injects the Context7 key intocontext7requests 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.
plugins.toml — no secrets in YAML.
- Hook code:
plugins/hooks/research_hooks.py - Registration:
agents.yamlandplugins/plugins.toml
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: failso they never fail open.