memory.HooksManager. This page covers the live praisonaiagents.hooks package, which fires automatically around an Agent’s tool/LLM calls. The standalone memory.HooksManager only runs when you call .execute() yourself.add_hook, emit with fire_hook. add_hook, remove_hook, has_hook, and get_default_registry manage subscribers; fire_hook is the emission counterpart a runtime component calls at a real state transition so those subscribers run. fire_hook() targets the process-wide default registry by default — pass registry= for a scoped one. See fire_hook.Quick Start
Simple Usage
add_hook and any agent picks it up automatically:Noneor no return → AllowFalse→ Deny"reason"→ Deny with custom message
With HooksConfig
HooksConfig to a specific agent for scoped hooks:API Surface
The simplified API covers registering, inspecting, and emitting hooks — all import frompraisonaiagents.hooks.
fire_hook() at real state transitions, so subscribing with add_hook is all a plugin author needs. See fire_hook for the emission API.
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, times out, has no callable (aFunctionHook whose func is None), or — for command hooks — exits with an unexpected exit code (anything other than 0 = allow or 2 = blocking; e.g. 127 command-not-found, 126 not-executable) now counts as Deny, not Allow. This applies even when the command already printed {"decision": "allow"} on stdout — a gating hook that crashes after emitting allow-JSON has not actually approved the call, so the runner denies it. Every hook lifecycle path returns HookResult(decision="deny", reason=...) on error — a broken BEFORE_TOOL gate no longer lets the tool call through by accident.
Command Hook Exit Codes
A shell-command hook communicates its verdict through both its exit code and its stdout JSON — the runner requires them to agree onallow.
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.
Thread Safety
HookRegistry guards its per-event hook lists with a re-entrant lock, so register / unregister / clear / enable_hook / disable_hook and get_hooks are safe under concurrent access from multiple threads. get_hooks snapshots the list under the lock before filtering, so a concurrent unregister / clear on another thread can no longer raise RuntimeError: list changed size during iteration or skip / duplicate a hook mid-iteration. The lock is an RLock, so a hook callback that registers or unregisters other hooks (a legitimate re-entrant pattern) still works.
This matters most for the process-wide default registry (the one you get via get_default_registry() or the module-level add_hook-style helpers) — it is shared across every agent in the process, so plugins registering hooks at import time can race an agent iterating hooks on another thread. As of PraisonAI PR #4634 that race is fixed. fire_hook() targets that default registry too; pass registry= for a scoped one.
Available Hook Events
The events most agents will ever need are the agent / tool / LLM / error / session ones — start here.before_llm, after_llm, model_fallback), plugin lifecycle hooks (on_init, on_shutdown — now emitted by PluginManager.register/unregister), message-level bot hooks (message_received, message_sending, message_sent, message_undelivered; before_message / after_message / tool_result_persist are aliases of live events), 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, subagent_stop), and kanban task hooks.See Hook Events for the complete reference with input dataclasses and examples for each, and fire_hook for the sibling emitter that delivers these events to subscribers.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.
before_llm and after_llm fire on both chat() and achat(). See Hook Events → LLM Events for the parity note and blocking semantics.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
on_step and on_tool_call are observers, wired into the same middleware chain that powers middleware=[...]:on_stepmaps to theafter_modelslot and receives theModelResponsefor that step — once per model call, not per token.on_tool_callmaps to thebefore_toolslot and receives aToolRequest(.tool_name,.arguments) before every tool the agent runs.- Return values are ignored — the
ModelResponse/ToolRequestalways passes through unchanged, so an observer can never corrupt the run. To short-circuit or rewrite, usemiddleware=[...](function-style hooks that returnHookResult) instead. - Async callbacks are safe — an
async defcallback is awaited automatically. No manual wrapping needed. middlewareruns beforeon_tool_call. Anymiddleware=[...]entry fires first, then theon_tool_callobserver sees the (possibly rewritten) request.
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
Use on_step / on_tool_call for logging, middleware for control
Use on_step / on_tool_call for logging, middleware for control
on_step and on_tool_call are observers — their return value is ignored, so use them for logging, metrics, and tracing. When you need to control the run (rewrite a request, block a tool, retry a model call), use middleware=[...] with function-style hooks that return HookResult.Ordering rule: middleware always runs before on_tool_call, so the observer sees whatever the middleware chain produced.Keep hooks lightweight
Keep hooks lightweight
Return None to allow, string to deny
Return None to allow, string to deny
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
sequential=True when it needs to mutate the payload via modified_input.Hooks fail closed on error, timeout, missing callable, or unexpected exit
Hooks fail closed on error, timeout, missing callable, or unexpected exit
BEFORE_TOOL (or any) hook fails closed — returns HookResult(decision="deny", reason=...) and the tool does not execute — on any of four triggers:- Raises an exception.
- Times out.
- Has no callable — a
FunctionHookwhosefunc is Nonedenies with"Hook '<name>' has no callable". - For command hooks, exits with an unexpected exit code — anything other than
0(allow) or2(blocking), e.g.127command-not-found or126not-executable.allow-JSON printed on stdout before the crash is discarded; explicitdeny-JSON keeps itsreason.
GuardrailChain and closes a silent pass-through where a buggy security hook used to allow the call through. Whenever any trigger fires on BEFORE_TOOL, the viewer sees "Execution of {tool_name} was blocked by security policy." 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
{"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.Use on_step / on_tool_call for logging, middleware for control
Use on_step / on_tool_call for logging, middleware for control
on_step and on_tool_call are observers — their return values are ignored, so they can only watch, never change or stop a run. Use them for logging, metrics, and tracing.middleware=[...] are controllers — function-style hooks that return HookResult to rewrite payloads or short-circuit the call. Use middleware when you need to block, retry, or mutate.When both are present on the same tool, middleware runs first, then the on_tool_call observer.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.
