Quick Start
1
Simple Usage
Register a hook with Hook return values:
add_hook and any agent picks it up automatically:Noneor no return → AllowFalse→ Deny"reason"→ Deny with custom message
2
With HooksConfig
Attach a
HooksConfig to a specific agent for scoped hooks:Which Hook Point Should I Use?
Pick the lifecycle event that matches what you want to observe or block.How It Works
A hook that raises or times out now counts asDeny, not Allow. Every hook lifecycle path returns HookResult(decision="deny", reason=...) on error — a crashing BEFORE_TOOL gate no longer lets the tool call through by accident.
Sequential vs Parallel Hooks
A hook’s execution mode decides whether it can rewrite the payload. Sequential hooks run one after another and theirmodified_input is applied back to the payload. Parallel hooks run concurrently and cannot mutate the payload — use them for read-only observers (metrics, logging, tracing). If your hook needs to rewrite the request (redact secrets, inject headers, edit messages), register it with sequential=True.
Available Hook Events
The events most agents will ever need are the agent / tool / LLM / error / session ones — start here.This is the core lifecycle subset. The SDK ships ~40 events in total — including LLM lifecycle hooks (
before_llm, after_llm, model_fallback), plugin/system hooks (on_init, on_shutdown), tool-result persistence (tool_result_persist), message-level bot hooks (before_message, after_message, message_received, message_sending, message_sent), gateway hooks (gateway_start, gateway_stop), compaction hooks (before_compaction, after_compaction), permission/config/auth hooks (on_permission_ask, on_config, on_auth), schedule hooks (schedule_add, schedule_remove, schedule_trigger), background-job hooks (job_completed), kanban task hooks, and Claude-Code-parity events (user_prompt_submit, notification, subagent_stop, setup).See Hook Events for the complete reference with input dataclasses and examples for each.on_retry is emitted once per retryable error, just before the back-off sleep. It runs whether the LLM call is agent.chat(...) (sync) or await agent.achat(...) (async). Sync-registered callbacks on the async path are run in a thread executor — they cannot block the event loop.
Since PraisonAI PR #3908,
before_llm and after_llm fire on both chat() and achat(). See Hook Events → LLM Events for the parity note and blocking semantics.Two lookalike fields. Function-style hooks return
HookResult and set modified_input to rewrite the payload. Shell-command hooks parse into HookOutput and use modified_data for the same purpose. When you write a hook in Python, always use HookResult(decision="allow", modified_input={...}) — the internal Agent code reads .modified_input on hook results.Configuration Options
HooksConfig SDK Reference
Full parameter reference for HooksConfig
Common Patterns
Security Filtering
Audit Logging
Redact Secrets from Tool Output
Rewrite the tool result the model sees — scrub API keys before they ever reach the LLM. Returning a value from anafter_tool hook replaces event_data.tool_output.
Block a Tool Result via GuardrailBlocked
RaiseGuardrailBlocked inside after_tool to stop a result reaching the model — mirrors the block path already available on before_tool.
after_tool returns are honoured as of PraisonAI PR #3969 — before that release the return value was silently discarded, so the hook could observe but never rewrite or block. Both the sync (agent.chat(...)) and async (await agent.achat(...)) tool-execution paths now read back event_data.tool_output and honour GuardrailBlocked.Tool Matching with HookRegistry
Redact a Tool Result
Rewriteevent_data.tool_output in place to scrub a secret, or return HookResult.block(reason) to suppress the result entirely.
HookEvent.BEFORE_TOOL and HookEvent.AFTER_TOOL now fire on both the sync (agent.chat(...)) and async (await agent.achat(...)) tool-execution paths. When a BEFORE_TOOL hook blocks a call, the tool returns "Execution of {tool_name} was blocked by security policy." on either path. AFTER_TOOL results aggregate context onto the tool output (string concat, or the _additional_context key on a dict result). The check costs nothing when no hooks are registered.AFTER_TOOL can also rewrite or block the result (PraisonAI PRs #3968 / #3969). Mutate event_data.tool_output in place to redact the value the model sees, or return HookResult.block(reason) to suppress it. See Redact / Block Tool Output.Best Practices
Keep hooks lightweight
Keep hooks lightweight
Hooks run synchronously before/after each operation. Avoid network calls or heavy computation inside hook functions — use async queues for heavy processing.
Return None to allow, string to deny
Return None to allow, string to deny
The simplest hook contract: return nothing (or
None) to allow, return a string with a reason to block. This keeps hooks readable.Use add_hook for global rules, HooksConfig for per-agent rules
Use add_hook for global rules, HooksConfig for per-agent rules
add_hook registers hooks globally — all agents in the process obey them. Use HooksConfig when you need different rules per agent.Sequential for rewriters, parallel for observers
Sequential for rewriters, parallel for observers
Parallel is the default and is faster. Only mark a hook
sequential=True when it needs to mutate the payload via modified_input.Hooks fail closed on exception or timeout
Hooks fail closed on exception or timeout
A
BEFORE_TOOL (or any) hook that raises or times out now returns HookResult(decision="deny", reason=...) — the tool does not execute. This matches GuardrailChain and closes a silent-pass-through where a buggy security hook used to allow the call through. Set agent._strict_hooks = True in tests to also surface the underlying exception, not just the deny.Tool errors don't fail closed — hooks do
Tool errors don't fail closed — hooks do
Tool failures are tolerated: a tool that raises or returns a non-JSON result is fed back to the model as
{"error": ...} / {"result": ...} and the run continues. Hooks are the opposite — a raising or timed-out hook denies the call. Keep security gates in hooks, not in tool bodies.before_agent / after_agent / BEFORE_TOOL_DEFINITIONS cost nothing when unregistered
before_agent / after_agent / BEFORE_TOOL_DEFINITIONS cost nothing when unregistered
before_agent, after_agent, and BEFORE_TOOL_DEFINITIONS cost nothing when no hook is registered — the runtime checks has_hooks() before building the input (including os.getcwd(), the tools list, and any deep-copy of tool definitions) on both sync (chat) and async (achat) paths. Register these hooks in production without a per-turn overhead concern.Related
Hook Events
Complete list of ~40 events with input dataclasses and examples
Guardrails
Validate agent output quality with automatic retry
Callbacks
Observe agent events for UI and logging purposes

