Skip to main content
Hooks intercept agent actions at lifecycle points so you can log, modify, or block them without changing agent code.
The user sends a request; hooks intercept tools and lifecycle steps without changing agent code.

Quick Start

1

Simple Usage

Register a hook with add_hook and any agent picks it up automatically:
Hook return values:
  • None or no return → Allow
  • False → 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 as Deny, 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 their modified_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.
Registering a mutating hook without sequential=True looks correct but silently no-ops — the runner runs it in parallel with sibling hooks, so its modified_input is discarded. As of PraisonAI PR #4034 the runtime logs a warning when a parallel hook returns a non-empty modified_input: "Parallel hook '<name>' returned modified_input which is discarded; register it with sequential=True to apply it." Watch your logs when adopting a new mutating hook.

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 an after_tool hook replaces event_data.tool_output.

Block a Tool Result via GuardrailBlocked

Raise GuardrailBlocked 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

Rewrite event_data.tool_output in place to scrub a secret, or return HookResult.block(reason) to suppress the result entirely.
See Redact / Block Tool Output for the plugin-style equivalent.
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

Hooks run synchronously before/after each operation. Avoid network calls or heavy computation inside hook functions — use async queues for heavy processing.
The simplest hook contract: return nothing (or None) to allow, return a string with a reason to block. This keeps hooks readable.
add_hook registers hooks globally — all agents in the process obey them. Use HooksConfig when you need different rules per agent.
Parallel is the default and is faster. Only mark a hook sequential=True when it needs to mutate the payload via modified_input.
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 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, 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.

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