Core Concepts
Hooks
Intercept and modify agent behavior at every lifecycle point.
Hooks let you observe and control agent behavior without modifying core logic. They fire at specific points in the agent lifecycle and can continue, skip, abort, or retry the current operation.
Hook events
| Event | When it fires |
|---|---|
BEFORE_TOOL | Before a tool is called |
AFTER_TOOL | After a tool returns successfully |
AFTER_TOOL_FAILURE | After a tool call fails |
PRE_COMPACT | Before context compaction |
AFTER_COMPACT | After context compaction |
USER_PROMPT_SUBMIT | When a user message is submitted |
STOP | When the agent stops |
SUBAGENT_STOP | When a sub-agent stops |
ON_ERROR | On unhandled errors |
BEFORE_AGENT_CREATION | Before an agent instance is created |
AFTER_AGENT_CREATION | After an agent instance is created |
ANY | Wildcard — fires on every event |
Hook actions
Each hook returns a HookResult with one of these actions:
| Action | Effect |
|---|---|
CONTINUE | Proceed normally (default) |
SKIP | Skip the current operation |
ABORT | Stop execution and raise HookAbortError |
RETRY | Retry the current tool call |
RETRY_WITH_COMPACT | Compact context, then retry |
Writing a hook
A hook is an async function that receives a HookContext and returns a HookResult (or None to continue):
from zgent import HookAction, HookContext, HookResult
async def log_tool_calls(ctx: HookContext) -> HookResult | None:
print(f"Tool called: {ctx.tool_name}({ctx.tool_input})")
return None # continue normallyRegistering hooks
On the definition
from zgent import AgentDefinition, AgentEngine
from zgent import HookEvent
definition = AgentDefinition(
system_prompt="You are a helpful assistant.",
engine_type=AgentEngine.CLAUDE,
hooks={
HookEvent.BEFORE_TOOL: [log_tool_calls],
HookEvent.AFTER_TOOL: [check_tool_output],
},
)With on_event
definition.on_event(HookEvent.BEFORE_TOOL, log_tool_calls)
definition.on_event(HookEvent.AFTER_TOOL, check_tool_output)Example: auto-approve safe tools
SAFE_TOOLS = {"Read", "Glob", "Grep"}
async def auto_approve(ctx: HookContext) -> HookResult | None:
if ctx.tool_name in SAFE_TOOLS:
return HookResult(action=HookAction.CONTINUE)
# Block dangerous tools
if ctx.tool_name == "Bash":
return HookResult(
action=HookAction.ABORT,
reason="Bash execution is not allowed.",
)
return NoneExample: modify tool input
async def inject_credentials(ctx: HookContext) -> HookResult | None:
if ctx.tool_name == "WebFetch" and ctx.tool_input:
ctx.tool_input["headers"] = {"Authorization": "Bearer xxx"}
return HookResult(
action=HookAction.CONTINUE,
modified_input=ctx.tool_input,
)
return NoneHook chain behavior
When multiple hooks are registered for the same event, they execute in order. If any hook returns ABORT, execution stops immediately. Otherwise, results are aggregated — modified outputs are combined.