> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Hooks

> Intercept, log, or block agent actions at any lifecycle point — before tools run, after LLM responds, on error

Hooks intercept agent actions at lifecycle points so you can log, modify, or block them without changing agent code.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.hooks import add_hook

@add_hook("before_tool")
def log_tools(event_data):
    print(f"Running tool: {event_data.tool_name}")

agent = Agent(name="SecureAssistant", instructions="You are a helpful assistant.")
agent.start("Help me organise my files")
```

The user sends a request; hooks intercept tools and lifecycle steps without changing agent code.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Agent Lifecycle"
        INPUT["📥 Input"] --> BTD["🪝 BEFORE_TOOL_DEFINITIONS\nhook"]
        BTD --> LLM["🧠 LLM Call"]
        LLM --> BEFORE["🪝 BEFORE_TOOL\nhook"]
        BEFORE --> TOOL["⚙️ Tool\nExecution"]
        TOOL --> AFTER["🪝 AFTER_TOOL\nhook"]
        AFTER --> OUTPUT["📤 Output"]
    end

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef hook fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef success fill:#10B981,stroke:#7C90A0,color:#fff

    class INPUT,OUTPUT agent
    class TOOL,LLM tool
    class BTD,BEFORE,AFTER hook
```

## Quick Start

<Steps>
  <Step title="Simple Usage">
    Register a hook with `add_hook` and any agent picks it up automatically:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonaiagents.hooks import add_hook

    @add_hook('before_tool')
    def log_tools(event_data):
        print(f"Running tool: {event_data.tool_name}")

    @add_hook('before_tool')
    def block_delete(event_data):
        if "delete" in event_data.tool_name.lower():
            return "Delete operations are blocked"

    agent = Agent(
        name="SecureAssistant",
        instructions="You are a helpful assistant."
    )

    agent.start("Help me organize my files")
    ```

    Hook return values:

    * `None` or no return → Allow
    * `False` → Deny
    * `"reason"` → Deny with custom message
  </Step>

  <Step title="With HooksConfig">
    Attach a `HooksConfig` to a specific agent for scoped hooks:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, HooksConfig

    def log_step(event_data):
        print(f"Step: {event_data}")

    def on_tool(event_data):
        print(f"Tool called: {event_data.tool_name}")

    agent = Agent(
        name="MonitoredAgent",
        instructions="You are a helpful assistant.",
        hooks=HooksConfig(
            on_step=log_step,
            on_tool_call=on_tool,
        )
    )

    agent.start("Write a haiku about Python")
    ```
  </Step>
</Steps>

***

## Which Hook Point Should I Use?

Pick the lifecycle event that matches what you want to observe or block.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start([What do you want to do?]) --> Q1{At which point?}
    Q1 -->|Inspect / block a tool| A[before_tool<br/>gate tool calls]
    Q1 -->|React to tool result| B[after_tool<br/>log or transform output]
    Q1 -->|Shape tools sent to LLM| C[before_tool_definitions<br/>filter the tool list]
    Q1 -->|Inspect the prompt| D[before_llm<br/>modify request]
    Q1 -->|React to a response| E[after_llm<br/>post-process reply]
    Q1 -->|Handle failures| F[on_error<br/>catch and recover]

    classDef start fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef question fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef answer fill:#10B981,stroke:#7C90A0,color:#fff

    class Start start
    class Q1 question
    class A,B,C,D,E,F answer
```

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant Hook
    participant Tool

    User->>Agent: start("task")
    Agent->>Hook: BEFORE_TOOL(event_data)
    alt Allow
        Hook-->>Agent: None (allow)
        Agent->>Tool: execute()
        Tool-->>Agent: result
        Agent->>Hook: AFTER_TOOL(result)
        Hook-->>Agent: None (allow)
        Agent-->>User: response
    else Deny
        Hook-->>Agent: "reason" (deny)
        Agent-->>User: blocked — "reason"
    end
```

### Available Hook Events

The events most agents will ever need are the agent / tool / LLM / error / session ones — start here.

| Event                     | When it fires                                                             |
| ------------------------- | ------------------------------------------------------------------------- |
| `before_agent`            | Before the agent starts a turn                                            |
| `after_agent`             | After the agent completes a turn                                          |
| `before_tool`             | Before any tool executes                                                  |
| `after_tool`              | After a tool returns a result                                             |
| `before_tool_definitions` | After tool list assembled, before sent to LLM — shape what the model sees |
| `before_llm`              | Before an LLM API call                                                    |
| `after_llm`               | After an LLM API response                                                 |
| `on_error`                | When any error occurs                                                     |
| `on_retry`                | Before a retry attempt (sync + async LLM paths)                           |
| `session_start`           | When a session begins                                                     |
| `session_end`             | When a session ends                                                       |

<Note>
  This is the **core lifecycle** subset. The SDK ships \~40 events in total — including 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](/docs/docs/features/hook-events) for the complete reference with input dataclasses and examples for each.
</Note>

`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.

<Note>
  **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.
</Note>

***

## Configuration Options

<Card title="HooksConfig SDK Reference" icon="code" href="/docs/docs/sdk/reference/python/classes/HooksConfig">
  Full parameter reference for HooksConfig
</Card>

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, HooksConfig

agent = Agent(
    instructions="...",
    hooks=HooksConfig(
        on_step=my_step_callback,
        on_tool_call=my_tool_callback,
        middleware=[my_middleware],
    )
)
```

| Option         | Type               | Default | Description                           |
| -------------- | ------------------ | ------- | ------------------------------------- |
| `on_step`      | `Callable \| None` | `None`  | Called on each agent step             |
| `on_tool_call` | `Callable \| None` | `None`  | Called before each tool execution     |
| `middleware`   | `List`             | `[]`    | Middleware list applied to all events |

***

## Common Patterns

### Security Filtering

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.hooks import add_hook

BLOCKED_TOOLS = {"delete_file", "execute_command", "drop_table"}

@add_hook('before_tool')
def security_filter(event_data):
    if event_data.tool_name in BLOCKED_TOOLS:
        return f"Tool '{event_data.tool_name}' is not allowed in this environment"

agent = Agent(
    name="SafeAgent",
    instructions="Help users manage their documents safely."
)

agent.start("Clean up the old log files")
```

### Audit Logging

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import json
from datetime import datetime
from praisonaiagents import Agent
from praisonaiagents.hooks import add_hook

@add_hook('before_tool')
def audit_before(event_data):
    print(json.dumps({
        "ts": datetime.utcnow().isoformat(),
        "event": "before_tool",
        "tool": event_data.tool_name,
    }))

@add_hook('after_tool')
def audit_after(event_data):
    print(json.dumps({
        "ts": datetime.utcnow().isoformat(),
        "event": "after_tool",
        "tool": event_data.tool_name,
    }))

agent = Agent(
    name="AuditAgent",
    instructions="Process user requests with full audit trail."
)

agent.start("Search for the latest AI research papers")
```

### Tool Matching with HookRegistry

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult

registry = HookRegistry()

@registry.on(HookEvent.BEFORE_TOOL)
def file_guard(event_data) -> HookResult:
    if event_data.tool_name.startswith("write_"):
        print(f"Write operation: {event_data.tool_name}")
    return HookResult.allow()

agent = Agent(
    name="GuardedAgent",
    instructions="You are a helpful assistant.",
    hooks=registry
)

agent.start("Create a summary and save it to output.txt")
```

<Note>
  `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.
</Note>

***

## Best Practices

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="Set _strict_hooks=True in tests">
    By default, hook exceptions are logged as warnings and execution continues. Set `agent._strict_hooks = True` in tests so hook failures surface immediately as errors.
  </Accordion>

  <Accordion title="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.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonaiagents.hooks import add_hook

    # No before_agent / after_agent hook registered anywhere in the process.
    # The runtime skips input construction entirely — no os.getcwd(), no tools list build.
    agent = Agent(name="Fast", instructions="Be helpful.")
    agent.start("Say hi")

    # Only pay the cost when you actually opt in:
    @add_hook("after_agent")
    def log_turn(event_data):
        print(f"Turn completed by {event_data.agent_name} in {event_data.execution_time_ms:.0f}ms")

    agent.start("Say hi again")   # Now the input is built and after_agent fires.
    ```
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={3}>
  <Card title="Hook Events" icon="webhook" href="/docs/docs/features/hook-events">
    Complete list of \~40 events with input dataclasses and examples
  </Card>

  <Card title="Guardrails" icon="shield-halved" href="/docs/docs/features/guardrails">
    Validate agent output quality with automatic retry
  </Card>

  <Card title="Callbacks" icon="circle-nodes" href="/docs/docs/features/callbacks">
    Observe agent events for UI and logging purposes
  </Card>
</CardGroup>
