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

# Plugins

> Extend agent functionality with single-file plugins

Plugins add tools, hooks, and guardrails that can rewrite — or now block — what an agent sees and does; drop a Python file in `~/.praisonai/plugins/` and load it in one line.

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

discover_and_load_plugins()

agent = Agent(
    name="Assistant",
    instructions="Help users with weather queries.",
    tools=["get_weather"],
)
agent.start("What's the weather in Paris?")
```

The user asks a question; plugins add tools and hooks before the agent answers.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Plugin[🔧 Plugin file] --> Load[discover_and_load_plugins]
    Load --> Agent[🤖 Agent gains tools + hooks]

    classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef output fill:#10B981,stroke:#7C90A0,color:#fff
    class Plugin input
    class Load process
    class Agent output
```

## Quick Start

<Steps>
  <Step title="Simple Usage">
    Create `~/.praisonai/plugins/my_tools.py`:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    """
    Plugin Name: My Tools
    Description: Custom tools for my agent
    Version: 1.0.0
    """

    from praisonaiagents import tool

    @tool
    def get_weather(city: str) -> str:
        """Get weather for a city."""
        return f"Sunny in {city}, 22°C"
    ```

    Load and run:

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

    discover_and_load_plugins()

    agent = Agent(
        name="Assistant",
        instructions="Help users",
        tools=["get_weather"],
    )
    agent.start("What's the weather in Paris?")
    ```
  </Step>

  <Step title="With Configuration">
    Point the environment (or config file) at plugins and Agent construction wires them for you — no explicit `plugins.enable()` call needed:

    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    export PRAISONAI_PLUGINS=true
    ```

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

    agent = Agent(name="Assistant", instructions="Help users")
    agent.start("Hello!")   # entry-point plugins are wired in before this runs
    ```

    Prefer to turn plugins on in code? Call `plugins.enable(...)` before creating the agent:

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

    plugins.enable(["logging", "metrics"])

    agent = Agent(name="Assistant", instructions="Help users")
    agent.start("Hello!")
    ```
  </Step>
</Steps>

<Note>
  Place plugins in `~/.praisonai/plugins/` (user-wide) or `./.praisonai/plugins/` (project-specific).
</Note>

<Note>
  `discover_and_load_plugins()` loads and registers plugins. To read plugin metadata **without loading** (e.g. to build a config-validation gate), use `discover_plugins()` and the capability manifest fields — see [Static Capability Manifest](#static-capability-manifest).
</Note>

<Warning>
  `plugins.enable()` auto-discovers filesystem and entry-point plugins only when `PRAISONAI_ALLOW_PLUGIN_DISCOVERY=true` (or `1`/`yes`). Without this env var, `plugins.enable()` still bridges plugins you register manually via `PluginManager.register(...)`, but no directory or entry-point scan runs.
</Warning>

***

## How It Works

`plugins.enable()` auto-calls `wire_into_hook_registry()`, which registers each enabled plugin's lifecycle methods on the default hook registry the agent consults at runtime — and Agent init calls this for you when the env var or config file requests it (see [Auto-Enable from Env or Config](#auto-enable-from-env-or-config)).

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Enable as plugins.enable()
    participant Bridge as wire_into_hook_registry
    participant Registry as Default HookRegistry
    participant Agent
    participant Plugin

    User->>Enable: plugins.enable()
    Enable->>Bridge: auto-called
    Bridge->>Registry: register before_agent, before_llm, ...
    User->>Agent: agent.start("...")
    Agent->>Registry: fire BEFORE_AGENT
    Registry->>Plugin: run before_agent(prompt, ctx)
    Plugin-->>Registry: modified prompt (in-place)
    Registry-->>Agent: HookResult.allow()
    Agent-->>User: response
```

The hooks fire in this order around each agent run:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    INPUT["📥 User Input"] --> BA["🪝 before_agent"]
    BA --> BTD["🪝 before_tool_definitions"]
    BTD --> LLM["🧠 LLM"]
    LLM --> BT["🪝 before_tool"]
    BT --> TOOL["🔧 Tool"]
    TOOL --> AT["🪝 after_tool"]
    AT --> AA["🪝 after_agent"]
    AA --> OUTPUT["📤 Response"]

    classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef hook fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff

    class INPUT,OUTPUT input
    class BA,BTD,BT,AT,AA hook
    class TOOL,LLM process
```

| Plugin type   | What it adds                                                                   |
| ------------- | ------------------------------------------------------------------------------ |
| **Tool**      | Functions registered via `@tool`                                               |
| **Hook**      | Lifecycle callbacks (`before_tool`, `before_tool_definitions`, `after_llm`, …) |
| **Guardrail** | Output validation on `after_agent`                                             |

***

## Choosing How to Load Plugins

Pick the loading method that fits your setup.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q[How is the plugin delivered?] --> A[Drop a file in a plugins dir]
    Q --> B[Enable a built-in by name]
    Q --> C[Register a hook at runtime]
    A --> R1[discover_and_load_plugins]
    B --> R2[plugins.enable list]
    C --> R3[add_hook decorator]

    classDef decision fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef option fill:#10B981,stroke:#7C90A0,color:#fff
    class Q,A,B,C decision
    class R1,R2,R3 option
```

## Plugin Locations

| Location                | Scope            |
| ----------------------- | ---------------- |
| `~/.praisonai/plugins/` | User-wide        |
| `./.praisonai/plugins/` | Project-specific |

***

## Security: Project-Plugin Trust Gate

A single-file plugin is the most privileged extension surface there is — it can hook every lifecycle event and intercept every tool call. So a plugin dropped into a **project's** `./.praisonai/plugins/*.py` (for example, one that arrived with a cloned repository) does **not** run until you opt in. This mirrors `PRAISONAI_ALLOW_LOCAL_TOOLS` for tools: a cloned repo carrying a malicious `.praisonai/plugins/exfil.py` stays inert by default.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{Where does the plugin live?}
    Q -->|./.praisonai/plugins/*.py| Gate{Gate open?}
    Q -->|~/.praisonai/plugins/*.py| Trust[✅ Loaded]
    Q -->|pip package / entry point| Trust
    Gate -->|PRAISONAI_ALLOW_PROJECT_PLUGINS=true| Trust
    Gate -->|plugins.allow_project_plugins: true| Trust
    Gate -->|default| Deny[🛑 Refused with warning]

    classDef decision fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef trust fill:#10B981,stroke:#7C90A0,color:#fff
    classDef deny fill:#8B0000,stroke:#7C90A0,color:#fff

    class Q,Gate decision
    class Trust trust
    class Deny deny
```

Open the gate with an environment variable (accepts `true`/`1`/`yes`/`on`):

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
export PRAISONAI_ALLOW_PROJECT_PLUGINS=true
```

Or in `.praisonai/config.yaml`:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
plugins:
  allow_project_plugins: true
```

| Behaviour             | Rule                                                                   |
| --------------------- | ---------------------------------------------------------------------- |
| **Default**           | Gate closed — project-local `.py` plugins are refused with a warning   |
| **User-global**       | `~/.praisonai/plugins/` plugins remain trusted (you placed them there) |
| **pip / entry point** | Installed entry-point plugins remain trusted                           |
| **Env precedence**    | `PRAISONAI_ALLOW_PROJECT_PLUGINS` wins over config                     |
| **Explicit off**      | `PRAISONAI_ALLOW_PROJECT_PLUGINS=false` overrides a `true` in config   |

<Warning>
  The gate judges a plugin by **where its file is located**, not where a symlink points. A repository-controlled symlink at `.praisonai/plugins/evil.py -> /tmp/evil.py` still counts as project-local and stays gated — it cannot slip past by resolving elsewhere.
</Warning>

***

## Inspect the Plugin Registry

`get_plugin_registry()` returns a truthful, unified view of every discoverable plugin from three real sources — no hardcoded entries. It reads without a running agent, so a CLI or health check can render it directly.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.plugins import get_plugin_registry

for p in get_plugin_registry():
    print(p["name"], p["source"], p["enabled"], p["hooks"])
```

Each entry is a dict: `{name, version, description, source, enabled, hooks}` (single-file entries also include `path`). The `source` field records provenance:

| `source`                          | Origin                                                                                                               |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `entry_point:<distribution-name>` | A pip package registering in the `praisonai.plugins` group                                                           |
| `registered`                      | A `Plugin` instance held by the `PluginManager`                                                                      |
| `single_file`                     | A `.py` file discovered on disk in `.praisonai/plugins/` or `~/.praisonai/plugins/` (metadata only — never executed) |

<Note>
  `enabled` reflects **both** the live manager state and the persisted config allow-list, so it renders correctly even before any agent starts.
</Note>

***

## Reload Plugins at Runtime

Edited a plugin, or dropped a new one in, and want it live in the current process? Run `praisonai plugins reload` — it forces rediscovery and rewires hooks so newly-added or edited plugins take effect without a restart. See the [Plugins CLI](/docs/cli/plugins#reload-plugins).

***

## Hook Plugin Example

Create `~/.praisonai/plugins/my_logger.py`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
"""
Plugin Name: My Logger
Description: Logs tool calls
Version: 1.0.0
Hooks: before_tool, after_tool
"""

from praisonaiagents.hooks import add_hook

@add_hook("before_tool")
def log_before(data):
    print(f"Calling: {data.tool_name}")

@add_hook("after_tool")
def log_after(data):
    print(f"Done: {data.tool_name}")
```

Create `~/.praisonai/plugins/tool_sandbox.py` to filter advertised tools:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
"""
Plugin Name: Tool Sandbox
Description: Filters tool definitions sent to the LLM
Version: 1.0.0
Hooks: before_tool_definitions
"""

from praisonaiagents.hooks import add_hook

BLOCKED = {"delete_file", "shell_exec"}

@add_hook("before_tool_definitions")
def sandbox_tools(data):
    data.tool_definitions[:] = [
        t for t in data.tool_definitions
        if t["function"]["name"] not in BLOCKED
    ]
```

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

discover_and_load_plugins()
agent = Agent(name="Assistant", instructions="Help users")
agent.start("Search for Python tutorials")
```

***

## Static Capability Manifest

Declare a plugin's capabilities in its header so onboarding, doctor and config validation can read them without importing the plugin's runtime.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Header[📄 Plugin header] --> Parser[🔍 parse_plugin_header]
    Parser --> Meta[📋 PluginMetadata]
    Meta --> Disc[🗺️ Discovery / list]
    Meta --> Val[✅ Config validation]
    Meta --> Gate[🚦 Capability gating]

    classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef parse fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef meta fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef out fill:#10B981,stroke:#7C90A0,color:#fff

    class Header input
    class Parser parse
    class Meta meta
    class Disc,Val,Gate out
```

Add four optional fields to the header — they parse as plain text, so the plugin's runtime is never imported to read them.

Create `~/.praisonai/plugins/telegram_channel.py`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
"""
Plugin Name: Telegram Channel
Description: Telegram bot adapter
Version: 1.0.0
Author: Your Name
Hooks: before_tool, after_tool
Dependencies: python-telegram-bot
Channels: telegram
Provides: send_message, get_updates
Config: api_key, timeout
Auto Enable When Configured: TELEGRAM_TOKEN
"""

from praisonaiagents import tool

@tool
def send_message(chat_id: str, text: str) -> str:
    """Send a Telegram message."""
    return f"Sent to {chat_id}: {text}"
```

### Field reference

| Field (header)                | Alias(es)                     | Type | Default | Purpose                                                                            |
| ----------------------------- | ----------------------------- | ---- | ------- | ---------------------------------------------------------------------------------- |
| `Channels`                    | —                             | list | `[]`    | Channels/adapters this plugin contributes (e.g. `telegram, slack`)                 |
| `Provides`                    | `Provides Tools`, `Tools`     | list | `[]`    | Tool/function names this plugin exposes (e.g. `get_weather, send_email`)           |
| `Config`                      | `Config Schema`               | list | `[]`    | Config keys this plugin reads (e.g. `api_key, timeout`)                            |
| `Auto Enable When Configured` | `auto_enable_when_configured` | list | `[]`    | Env/credential keys that, when set, auto-enable the plugin (e.g. `TELEGRAM_TOKEN`) |

### Read the manifest from code

`discover_plugins()` scans plugin directories and returns metadata dicts — the plugin body is never imported.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.plugins.discovery import discover_plugins

for meta in discover_plugins():
    print(
        meta["name"],
        meta.get("channels", []),
        meta.get("provides", []),
        meta.get("config", []),
        meta.get("auto_enable_when_configured", []),
    )
```

<Note>
  Omitted fields are absent from the raw `discover_plugins()` dict, so read them with `.get(key, [])`. `PluginMetadata.to_dict()` normalizes them to empty lists.
</Note>

### Rules

* Fields are **optional** — omitting them yields empty lists (safe default).
* Field keys are **case-insensitive** — `Channels`, `channels`, and `CHANNELS` all work.
* Aliases: `Provides` = `Provides Tools` = `Tools`; `Config` = `Config Schema`; `Auto Enable When Configured` = `auto_enable_when_configured`.
* The plugin file's runtime is **never imported** to read these fields — safe to scan untrusted plugin directories for a capability inventory.

### Do I need a manifest?

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q[Does the plugin expose tools or channels?] --> A[Single-file plugin with tools/channels]
    Q --> B[Hook-only plugin]
    A --> R1[✅ Declare a capability manifest]
    B --> R2[Optional — add fields only if useful]

    classDef decision fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef option fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
    class Q decision
    class A,B option
    class R1,R2 result
```

***

## Lifecycle-Method Plugins

Subclass `Plugin` and override a lifecycle method to transform prompts, messages, or responses. Call `plugins.enable()` — or set `PRAISONAI_PLUGINS=true` / `[plugins] enabled = true` and let Agent construction wire it — to activate the method at runtime.

Create `~/.praisonai/plugins/pii_redactor.py`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
"""
Plugin Name: PII Redactor
Description: Scrubs SSNs from LLM responses
Version: 1.0.0
"""
import re
from praisonaiagents import Plugin, PluginInfo, PluginHook

class PIIRedactor(Plugin):
    @property
    def info(self):
        return PluginInfo(
            name="pii_redactor",
            version="1.0.0",
            description="Scrubs SSNs from LLM responses",
            hooks=[PluginHook.AFTER_LLM],
        )

    def after_llm(self, response: str, usage: dict) -> str:
        return re.sub(r"\b\d{3}-\d{2}-\d{4}\b", "[SSN]", response)

def create_plugin():
    return PIIRedactor()
```

Load and enable:

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

plugins.enable()   # bridges lifecycle methods into the runtime hook registry

agent = Agent(name="Assistant", instructions="Help users.")
agent.start("My SSN is 123-45-6789")   # response is redacted before the user sees it
```

***

## Session & Error Lifecycle Plugins

Override `session_start`, `session_end`, `on_error`, `on_config`, or `on_auth` to react to session boundaries, errors, config, and credential resolution.

### Choosing a lifecycle event

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q[What do you want to react to?] --> A[Every session boundary]
    Q --> B[Errors during a run]
    Q --> C[Runtime config injection]
    Q --> D[Credential resolution]
    A --> R1[session_start / session_end]
    B --> R2[on_error]
    C --> R3[on_config]
    D --> R4[on_auth]

    classDef decision fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef option fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
    class Q decision
    class A,B,C,D option
    class R1,R2,R3,R4 result
```

### Observe sessions

`session_start` and `session_end` observe when a session opens and closes.

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

plugins.enable()

agent = Agent(name="Assistant", instructions="Help users.")
agent.start("Hello!")
```

Create `~/.praisonai/plugins/session_logger.py`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
"""
Plugin Name: Session Logger
Description: Records when sessions start and end
Version: 1.0.0
"""
from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook

class SessionLogger(Plugin):
    @property
    def info(self):
        return PluginInfo(
            name="session_logger",
            hooks=[PluginHook.SESSION_START, PluginHook.SESSION_END],
        )

    def session_start(self, context):
        print(f"session started: {context.get('session_name')}")

    def session_end(self, context):
        print(f"session ended: {context.get('reason')} after {context.get('total_turns')} turns")

def create_plugin():
    return SessionLogger()
```

### Observe errors

`on_error` observes errors during a run — use it to log without changing behavior.

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

plugins.enable()

agent = Agent(name="Assistant", instructions="Help users.")
agent.start("Do something that might fail")
```

Create `~/.praisonai/plugins/error_reporter.py`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
"""
Plugin Name: Error Reporter
Description: Logs errors during agent runs
Version: 1.0.0
"""
from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook

class ErrorReporter(Plugin):
    @property
    def info(self):
        return PluginInfo(
            name="error_reporter",
            hooks=[PluginHook.ON_ERROR],
        )

    def on_error(self, error_type, error_message, context):
        print(f"{error_type}: {error_message}")
        print(context.get("stack_trace"))

def create_plugin():
    return ErrorReporter()
```

### Rewrite config

`on_config` returns a `dict` to rewrite runtime configuration in place.

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

plugins.enable()

agent = Agent(name="Assistant", instructions="Help users.")
agent.start("Hello!")
```

Create `~/.praisonai/plugins/config_defaults.py`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
"""
Plugin Name: Config Defaults
Description: Injects a default temperature into runtime config
Version: 1.0.0
"""
from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook

class ConfigDefaults(Plugin):
    @property
    def info(self):
        return PluginInfo(
            name="config_defaults",
            hooks=[PluginHook.ON_CONFIG],
        )

    def on_config(self, config):
        config.setdefault("temperature", 0.2)
        return config

def create_plugin():
    return ConfigDefaults()
```

### Inject credentials

`on_auth` returns a `dict` of credentials — the bridge writes them back even when `credentials` starts as `None`, so first-time injection works.

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

plugins.enable()

agent = Agent(name="Assistant", instructions="Help users.")
agent.start("Call the authenticated API")
```

Create `~/.praisonai/plugins/token_injector.py`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
"""
Plugin Name: Token Injector
Description: Supplies credentials on first-time auth
Version: 1.0.0
"""
import os
from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook

class TokenInjector(Plugin):
    @property
    def info(self):
        return PluginInfo(
            name="token_injector",
            hooks=[PluginHook.ON_AUTH],
        )

    def on_auth(self, auth_type, credentials):
        return {"token": os.environ["MY_API_TOKEN"]}

def create_plugin():
    return TokenInjector()
```

## Message-Lifecycle Plugins

Override `before_message`, `after_message`, `message_sent`, or `message_undelivered` to react to every hop of a message's lifecycle — per-user policy, per-channel redaction, delivery telemetry, or dead-letter escalation.

### Choosing a message hook

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q[What do you want to react to?] --> A[Inbound message before agent sees it]
    Q --> B[Outbound reply before delivery]
    Q --> C[Successful delivery]
    Q --> D[Permanent delivery failure]
    A --> R1[before_message → MESSAGE_RECEIVED - can block/rewrite]
    B --> R2[after_message → MESSAGE_SENDING - can rewrite]
    C --> R3[message_sent → MESSAGE_SENT - observe-only]
    D --> R4[message_undelivered → MESSAGE_UNDELIVERED - observe-only, +error/notice_delivered]

    classDef decision fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef option fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
    class Q decision
    class A,B,C,D option
    class R1,R2,R3,R4 result
```

### Enriched payload keys

Every message-lifecycle method receives (at most) these keys:

| Key                | Type   | Present on                          | Description                                                                 |
| ------------------ | ------ | ----------------------------------- | --------------------------------------------------------------------------- |
| `content`          | `str`  | All four events                     | Always emitted (empty string when the input has none).                      |
| `platform`         | `str`  | All four events (when set)          | Adapter/channel identifier (e.g. `"telegram"`, `"slack"`, `"cli"`).         |
| `sender_id`        | `str`  | Inbound (when set)                  | Stable identifier for the user/agent that sent the message.                 |
| `channel_id`       | `str`  | Inbound + outbound (when set)       | Stable identifier for the channel/thread/DM.                                |
| `channel_type`     | `str`  | Inbound (when set)                  | e.g. `"dm"`, `"group"`, `"channel"`, `"broadcast"`.                         |
| `message_id`       | `str`  | Inbound + `message_sent` (when set) | Provider-native message id for correlation.                                 |
| `session_id`       | `str`  | All four events (when set)          | PraisonAI session id — pair with `session_start` / `session_end`.           |
| `error`            | `str`  | **`message_undelivered` only**      | Failure classification / message from the delivery pipeline.                |
| `notice_delivered` | `bool` | **`message_undelivered` only**      | `True` when a user-visible "your message wasn't delivered" notice was sent. |

<Note>
  Absent fields are **skipped** — the bridge does not fabricate empty strings for keys the concrete input doesn't carry. Read fields with `.get(key)`, not subscripting, so a plugin written against inbound input degrades cleanly on outbound events.
</Note>

### React per user, per channel, and per delivery

Lead with the outcome: rate-limit a noisy sender, count deliveries per channel, or escalate a dead letter.

<CodeGroup>
  ```python ~/.praisonai/plugins/per_user_rate_limit.py theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook, PluginDecision

  class PerUserRateLimit(Plugin):
      def __init__(self):
          self._counts = {}

      @property
      def info(self):
          return PluginInfo(name="per_user_rate_limit",
                            hooks=[PluginHook.MESSAGE_RECEIVED])

      def before_message(self, message):
          sender = message.get("sender_id")
          if sender is None:
              return message              # outbound/unknown — no scope to enforce
          self._counts[sender] = self._counts.get(sender, 0) + 1
          if self._counts[sender] > 20:
              return PluginDecision.deny(f"Rate limit exceeded for {sender}")
          return message

  def create_plugin():
      return PerUserRateLimit()
  ```

  ```python ~/.praisonai/plugins/delivery_counter.py theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook

  class DeliveryCounter(Plugin):
      def __init__(self):
          self._sent = {}

      @property
      def info(self):
          return PluginInfo(name="delivery_counter",
                            hooks=[PluginHook.MESSAGE_SENT])

      def message_sent(self, message):
          key = (message.get("platform"), message.get("channel_id"))
          self._sent[key] = self._sent.get(key, 0) + 1

  def create_plugin():
      return DeliveryCounter()
  ```

  ```python ~/.praisonai/plugins/dead_letter.py theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook

  class DeadLetter(Plugin):
      @property
      def info(self):
          return PluginInfo(name="dead_letter",
                            hooks=[PluginHook.MESSAGE_UNDELIVERED])

      def message_undelivered(self, message):
          # Payload adds error + notice_delivered on this hook only
          print(f"[dead-letter] {message.get('platform')} "
                f"channel={message.get('channel_id')} "
                f"error={message.get('error')} "
                f"notice_delivered={message.get('notice_delivered')}")

  def create_plugin():
      return DeadLetter()
  ```
</CodeGroup>

<Warning>
  * Absent fields are **skipped**, not emitted as `""` — always `.get(key)` and handle `None`.
  * `message_sent` and `message_undelivered` are **observe-only**; return values are ignored (no rewrite, no block).
  * The new hooks only fire when a plugin **overrides them** (`_overrides` guard) — plain plugins still incur zero cost on the two new events.
</Warning>

<Note>
  `message_undelivered` is the counterpart to the plain `MESSAGE_UNDELIVERED` hook already covered in [Undelivered Messages](/docs/features/undelivered-messages) and [Gateway → Undelivered Notice](/docs/features/gateway-undelivered-notice) — this hook exposes the same signal at the plugin surface, so `Plugin` subclass authors don't need to reach for the raw `HookRegistry`.
</Note>

***

## Function-Based Plugins (`FunctionPlugin`)

`FunctionPlugin` wraps plain functions into a plugin without subclassing — useful for one-off hooks or when the whole plugin is a single callable.

### When to reach for it

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q[How much do you need?] --> A[One or two hook callbacks]
    Q --> B[State, config, multiple hooks, session lifecycle]
    A --> R1[FunctionPlugin - wrap plain functions]
    B --> R2[Subclass Plugin - full base class]

    classDef decision fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef option fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
    class Q decision
    class A,B option
    class R1,R2 result
```

### Quick Start

Register a `FunctionPlugin` on the plugin manager, then start the agent — the callable runs before the agent sees the message.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, FunctionPlugin, PluginHook
from praisonaiagents.plugins import get_plugin_manager

def redact_secrets(message):
    text = message.get("content", "")
    return {"content": text.replace("sk-", "sk-****")}

get_plugin_manager().register(FunctionPlugin(
    name="redact_secrets",
    hooks={PluginHook.MESSAGE_RECEIVED: redact_secrets},
))

agent = Agent(name="Assistant", instructions="Help users.")
agent.start("here is my key sk-live-abc")   # agent sees redacted content
```

### Constructor

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
FunctionPlugin(
    name: str,
    hooks: Optional[Dict[PluginHook, Callable]] = None,
    version: str = "1.0.0",
    description: str = "",
)
```

| Parameter     | Type                         | Default   | Description                                                        |
| ------------- | ---------------------------- | --------- | ------------------------------------------------------------------ |
| `name`        | `str`                        | —         | Plugin name shown in the registry.                                 |
| `hooks`       | `Dict[PluginHook, Callable]` | `None`    | Maps each `PluginHook` enum value to the callable that handles it. |
| `version`     | `str`                        | `"1.0.0"` | Semantic version.                                                  |
| `description` | `str`                        | `""`      | What the plugin does.                                              |

### Hook dispatch table

Each key in `hooks={}` is a `PluginHook` enum value; the callable's return value drives the effect below.

| `PluginHook` key                 | Callable signature                                                 | Effect                                                                        |
| -------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| `PluginHook.BEFORE_TOOL`         | `fn(tool_name: str, args: dict) -> dict`                           | Rewrites tool args                                                            |
| `PluginHook.AFTER_TOOL`          | `fn(tool_name: str, result: Any) -> Any \| PluginDecision \| None` | Rewrites/redacts tool result, or `deny` blocks it before it reaches the model |
| `PluginHook.BEFORE_AGENT`        | `fn(prompt: str, context: dict) -> str`                            | Rewrites prompt                                                               |
| `PluginHook.AFTER_AGENT`         | `fn(response: str, context: dict) -> str`                          | Rewrites response                                                             |
| `PluginHook.MESSAGE_RECEIVED`    | `fn(message: dict) -> dict \| PluginDecision \| None`              | Rewrites `content`, or `deny` drops the inbound message                       |
| `PluginHook.MESSAGE_SENDING`     | `fn(message: dict) -> dict`                                        | Rewrites outbound `content`                                                   |
| `PluginHook.MESSAGE_SENT`        | `fn(message: dict) -> None`                                        | Observe-only                                                                  |
| `PluginHook.MESSAGE_UNDELIVERED` | `fn(message: dict) -> None`                                        | Observe-only (payload adds `error`, `notice_delivered`)                       |

### Message-lifecycle examples

Lead with the outcome: rate-limit a channel, log every delivery, or capture permanent failures.

<CodeGroup>
  ```python rate-limit a channel theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents import FunctionPlugin, PluginHook, PluginDecision
  from praisonaiagents.plugins import get_plugin_manager

  counts = {}
  def gate(message):
      ch = message.get("channel_id")
      if ch is None:
          return message
      counts[ch] = counts.get(ch, 0) + 1
      if counts[ch] > 20:
          return PluginDecision.deny(f"rate limit exceeded for {ch}")
      return message

  get_plugin_manager().register(FunctionPlugin(
      name="channel_rate_limit",
      hooks={PluginHook.MESSAGE_RECEIVED: gate},
  ))
  ```

  ```python log every successful delivery theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents import FunctionPlugin, PluginHook
  from praisonaiagents.plugins import get_plugin_manager

  def on_sent(message):
      print(f"[sent] {message.get('platform')} channel={message.get('channel_id')}")

  get_plugin_manager().register(FunctionPlugin(
      name="delivery_log",
      hooks={PluginHook.MESSAGE_SENT: on_sent},
  ))
  ```

  ```python capture permanent delivery failures theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents import FunctionPlugin, PluginHook
  from praisonaiagents.plugins import get_plugin_manager

  def on_dead(message):
      # error + notice_delivered are only added on the MESSAGE_UNDELIVERED bridge
      print(f"[dead] {message.get('channel_id')} err={message.get('error')} "
            f"notice_delivered={message.get('notice_delivered')}")

  get_plugin_manager().register(FunctionPlugin(
      name="dead_letter_log",
      hooks={PluginHook.MESSAGE_UNDELIVERED: on_dead},
  ))
  ```
</CodeGroup>

<Warning>
  * The `hooks` dict key must be a `PluginHook` enum value, not the string `"before_tool"` — a string key silently no-ops.
  * `FunctionPlugin` receives the **same enriched payload** as `Plugin` subclasses, so absent identity fields are **skipped**, not empty strings. Always use `.get(key)` and handle `None`.
  * `MESSAGE_SENT` / `MESSAGE_UNDELIVERED` callbacks are observe-only — return values are ignored (no rewrite, no block).
</Warning>

<Note>
  `FunctionPlugin`'s message-lifecycle dispatch (`MESSAGE_RECEIVED`, `MESSAGE_SENDING`, `MESSAGE_SENT`, `MESSAGE_UNDELIVERED`) is a recent addition ([commit `857ebbe`](https://github.com/MervinPraison/PraisonAI/commit/857ebbe344cc295a5f1c52d0167c039fae4c3332)) — earlier versions silently no-op'd these four keys, so users had to subclass `Plugin`.
</Note>

<Note>
  For the full payload contract (identity keys, per-event availability, `error` / `notice_delivered` special-cases), see [Message-Lifecycle Plugins](#message-lifecycle-plugins) — `FunctionPlugin` and `Plugin` subclasses share the exact same `_message_payload` bridge.
</Note>

***

## Auto-Enable from Env or Config

Agent construction auto-enables plugins when the environment or config file requests it — no explicit `plugins.enable()` call needed.

Set the env var, then any `Agent(...)` wires plugins before it runs:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
export PRAISONAI_PLUGINS=true
```

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

agent = Agent(name="Assistant", instructions="Help users")
agent.start("Hello!")   # plugins are already active
```

Or turn them on in `.praisonai/config.toml`:

```toml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
[plugins]
enabled = true          # or false, or ["logging", "metrics"]
```

<Tip>
  Want each plugin to receive its own options from one file? See [Configure Plugins from `.praisonai/config.yaml`](#configure-plugins-from-praisonai-config-yaml) for per-plugin option blocks.
</Tip>

Under the hood, Agent init calls `plugins.maybe_enable_from_config()`, which reads the env var and config file, then runs `enable(get_enabled_plugins())` exactly once per process.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q[How do you turn plugins on?] --> A[Set PRAISONAI_PLUGINS=true]
    Q --> B[Add plugins enabled = true in config.toml]
    Q --> C[Call plugins.enable in code]
    A --> R1[Agent init auto-enables]
    B --> R1
    C --> R2[Enabled immediately, before Agent init]

    classDef decision fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef option fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
    class Q decision
    class A,B,C option
    class R1,R2 result
```

Precedence: **explicit `plugins.enable(...)` > `PRAISONAI_PLUGINS` env var > `[plugins]` in `config.yaml` / `config.toml` > disabled**.

<Note>
  `maybe_enable_from_config()` is idempotent and runs at most once per process, so instantiating multiple agents is safe — plugins are wired a single time.
</Note>

<Warning>
  Auto-enable does **not** imply discovery. Filesystem and entry-point scanning still require `PRAISONAI_ALLOW_PLUGIN_DISCOVERY=true` (or `1`/`yes`). Without it, only plugins registered manually via `PluginManager.register(...)` are bridged.
</Warning>

<Tip>
  Verify auto-enable at runtime — constructing an Agent triggers the wiring:

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

  Agent(name="probe", instructions="noop")   # triggers maybe_enable_from_config
  print(plugins.is_enabled(), plugins.list_plugins())
  ```
</Tip>

***

## Suppress Plugins for One Run (Pure Mode)

The inverse of auto-enable — skip external-plugin discovery for a single run without touching persisted state.

Pass `--pure` (or `--no-plugins`) to `run`, `chat`, or `code`:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai run --pure "Summarise this file"
```

Or forced-off in embedded Python, regardless of the env var:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.plugins import PluginManager

manager = PluginManager(disabled=True)
manager.discover_entry_points()   # returns 0; no plugins loaded
```

Precedence: constructor `disabled=True` > `PRAISONAI_NO_PLUGINS` env var. The CLI flag is scoped and restored on return, and never mutates `.praisonai/config.yaml`.

See [Pure Mode](/docs/features/pure-mode) for the full guide.

***

## Configure Plugins from `.praisonai/config.yaml`

Turn plugins on and hand each one its own options from a single YAML file — no code.

`~/.praisonai/config.yaml`:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
plugins:
  enabled: true
  pii_guardrail:
    redact: [email, phone]
  memory_watchdog:
    interval: 30
```

`app.py`:

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

agent = Agent(name="Assistant", instructions="Help users.")
agent.start("My email is user@example.com")   # pii_guardrail redacts before the LLM sees it
```

<Note>
  Agent construction auto-loads the config, enables plugins, and hands `pii_guardrail` its `{redact: [email, phone]}` dict via `on_config`. Nothing else to wire up.
</Note>

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Config[".praisonai/config.yaml"] --> Loader["maybe_enable_from_config"]
    Loader --> Enable["enable(names, options_by_name)"]
    Enable --> Apply["apply_plugin_options"]
    Apply --> OnConfig["plugin.on_config(options)"]

    classDef config fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef loader fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef manager fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef plugin fill:#10B981,stroke:#7C90A0,color:#fff

    class Config config
    class Loader,Enable loader
    class Apply manager
    class OnConfig plugin
```

### Where the config file is found

```
1. .praisonai/config.toml   → .praisonai/config.yaml   → .praisonai/config.yml   (project-local)
2. praisonai.toml           → praisonai.yaml           → praisonai.yml           (project root)
3. ~/.praisonai/config.toml → ~/.praisonai/config.yaml → ~/.praisonai/config.yml (user global)
```

<Info>
  The first file found wins — TOML and YAML have equal standing; whichever appears first in the search stops the walk.
</Info>

<Warning>
  PyYAML is an *optional* dependency. Without it, `.yaml` configs return empty (debug log only) — install with `pip install pyyaml` to enable the YAML surface.
</Warning>

### The four shapes of `enabled`

| `enabled` value               | Meaning                                                                         | Example                             |
| ----------------------------- | ------------------------------------------------------------------------------- | ----------------------------------- |
| `true`                        | Enable every discovered plugin — **preserved** across the loader/CLI round-trip | `enabled: true`                     |
| `false`                       | Disable every plugin — **wins over per-plugin blocks**                          | `enabled: false`                    |
| `[names]` (non-empty list)    | Enable exactly these plugins                                                    | `enabled: [pii_guardrail, metrics]` |
| `"name"` (bare string, TOML)  | Enable exactly this one plugin                                                  | `enabled = "pii_guardrail"`         |
| *omitted* + per-plugin blocks | Enable each plugin block whose own `enabled` is not `false`                     | (see below)                         |

<Info>
  The JSON config schema advertises `boolean` and `array` for `enabled`; the loader also accepts a bare string at runtime for TOML users who prefer `enabled = "pii_guardrail"`.
</Info>

<Note>
  A bare `enabled: true` is now **preserved** — enabling a plugin while it is set is a no-op (leaving "all enabled" intact). You **cannot** run `praisonai plugins disable X` while `enabled: true`: the CLI refuses with a clear error, since collapsing "all" to an allow-list would silently disable every other plugin. Set `plugins.enabled` to an explicit list of names first, then disable individual plugins.
</Note>

### Per-plugin option blocks

Reserved keys (`enabled`, `auto_discover`, `directories`, `allow_project_plugins`) configure the plugin *system*; **any other key whose value is a mapping is treated as a per-plugin option map** delivered to that plugin's `on_config` hook.

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
plugins:
  enabled: true
  allow_project_plugins: true   # authorise ./.praisonai/plugins/*.py
  pii_guardrail:
    redact: [email, phone]
    enabled: true
  memory_watchdog:
    interval: 30
    enabled: false     # disable just this one; pii_guardrail still runs
```

<Note>
  `allow_project_plugins: true` opens the [project-plugin trust gate](#security-project-plugin-trust-gate) so single-file plugins under `./.praisonai/plugins/` load. The `PRAISONAI_ALLOW_PROJECT_PLUGINS` env var takes precedence over this config key.
</Note>

<Note>
  An explicit top-level `enabled: false` wins over any per-plugin block. Omit `enabled` at the top level and per-plugin blocks decide.
</Note>

### Reading options inside a plugin — `on_config`

`apply_plugin_options()` invokes `plugin.on_config(options)` for every enabled plugin that has a configured option map — errors in a single plugin are logged and do not abort delivery to the others.

Create `~/.praisonai/plugins/pii_guardrail.py`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
"""
Plugin Name: PII Guardrail
Description: Redacts configured fields before the LLM sees them
Version: 1.0.0
"""
from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook

class PIIGuardrail(Plugin):
    def __init__(self):
        self._fields = []

    @property
    def info(self):
        return PluginInfo(
            name="pii_guardrail",
            hooks=[PluginHook.ON_CONFIG, PluginHook.BEFORE_LLM],
        )

    def on_config(self, config):
        # config is the exact dict from `plugins.pii_guardrail:` in YAML.
        self._fields = list(config.get("redact", []))
        return config

    def before_llm(self, messages, params):
        # Use self._fields to scrub before the LLM call...
        return messages, params

def create_plugin():
    return PIIGuardrail()
```

<Note>
  `on_config` fires once per enable — the reserved `enabled` flag (if present in the plugin block) is preserved so plugins can read it too.
</Note>

### Deliver options from code — `options_by_name`

Skip the file and hand each plugin its options directly. `plugins.enable()` takes an `options_by_name` map that it delivers to each plugin's `on_config` hook:

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

plugins.enable(
    ["pii_guardrail"],
    options_by_name={"pii_guardrail": {"redact": ["email"]}},
)
```

<Note>
  `.praisonai/config.yaml` calls this for you — `maybe_enable_from_config()` runs `enable(get_enabled_plugins(), options_by_name=get_plugin_options())` under the hood. Call `get_plugin_options()` yourself to read the per-plugin maps the loader parsed.
</Note>

### Choose a plugin config surface

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q[How do you configure plugins?] --> A[One plugin, no options]
    Q --> B[Multiple plugins with options, checked into repo]
    Q --> C[Ephemeral per-shell override]
    A --> R1["plugins.enable('name')"]
    B --> R2[".praisonai/config.yaml"]
    C --> R3["PRAISONAI_PLUGINS env var"]

    classDef decision fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef option fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
    class Q decision
    class A,B,C option
    class R1,R2,R3 result
```

### Precedence

**Explicit `plugins.enable(...)` in code > `PRAISONAI_PLUGINS` env var > `[plugins]` in `config.yaml` / `config.toml` > disabled by default.**

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Code["plugins.enable(...) in code"] --> Env["PRAISONAI_PLUGINS env var"]
    Env --> File["[plugins] in config.yaml / config.toml"]
    File --> Off["disabled by default"]

    classDef top fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef mid fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef low fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef off fill:#8B0000,stroke:#7C90A0,color:#fff

    class Code top
    class Env,File mid
    class Off off
```

### Edge cases

* **PyYAML missing** — YAML configs silently return empty (debug log only); install `pyyaml` to enable.
* **Unknown reserved key** — the wrapper resolver's typo-suggestion validator only checks against `{enabled, auto_discover, directories, allow_project_plugins}`; per-plugin blocks (dicts) are always accepted.
* **Removed plugin block on reload** — the manager stores options with `replace=True` by default, so a block dropped from a later config no longer delivers stale options.
* **Plugin `on_config` raises** — logged as a warning; delivery to other plugins is not aborted.

### Config-write single source of truth

`praisonai plugins enable`/`disable` write to the **same file the runtime reads** — no separate JSON. The write target is resolved in this order:

```
1. .praisonai/config.toml   → .praisonai/config.yaml     (project-local)
2. praisonai.toml           → praisonai.yaml             (project root)
3. ~/.praisonai/config.toml → ~/.praisonai/config.yaml   (user global)
4. (none found)             → .praisonai/config.yaml     (default)
```

<Warning>
  Writing a `.toml` config requires `tomli_w`. Without it the CLI **fails fast** with a remediation hint (`Install tomli-w (pip install tomli-w), or convert the config to .praisonai/config.yaml.`) rather than silently writing a `.yaml` sidecar the runtime would ignore.
</Warning>

<Note>
  `praisonai plugins disable <name>` unloads a single-file plugin's **tools** as well as its hooks — `unload_plugin(module_name)` removes the functions that module contributed (tracked via a pre-exec registry snapshot), so `disable` truly removes the plugin's functionality for the rest of the run without touching tools owned by another plugin or the core.
</Note>

***

## How the Bridge Works

`plugins.enable()` auto-calls `wire_into_hook_registry()` — no manual step. Only lifecycle methods a plugin actually overrides (or declares in `PluginInfo.hooks`) are bridged, so a plugin with one guardrail never fires on every event. Return a new value and the bridge writes it back onto the payload in place. Errors in a lifecycle method are non-fatal, and `plugins.disable([...])` calls `unwire_from_hook_registry(name)` so the plugin truly stops firing.

<Note>
  The five `before_*` methods (`before_agent`, `before_llm`, `before_tool`, `before_tool_definitions`, `before_message`) also accept a deny/block decision instead of a rewrite — see [Blocking Plugins](#blocking-plugins-guardrails--policies). `after_tool` additionally accepts a rewrite **or** block — see [Redact / Block Tool Output](/docs/features/redact-tool-output).
</Note>

| Plugin method                                  | Hook event                | In-place mutation                                                                                                                                                                                                                                                   |
| ---------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `before_agent(prompt, ctx)`                    | `BEFORE_AGENT`            | Return a `str` → rewrites `data.prompt`                                                                                                                                                                                                                             |
| `after_agent(response, ctx)`                   | `AFTER_AGENT`             | Return a `str` → rewrites `data.response`                                                                                                                                                                                                                           |
| `before_llm(messages, params)`                 | `BEFORE_LLM`              | Return `(new_messages, ...)` → replaces `data.messages`                                                                                                                                                                                                             |
| `after_llm(response, usage)`                   | `AFTER_LLM`               | Return a `str` → rewrites `data.response`                                                                                                                                                                                                                           |
| `before_tool(name, args)`                      | `BEFORE_TOOL`             | Return a `dict` → replaces `data.tool_input`                                                                                                                                                                                                                        |
| `after_tool(name, result)`                     | `AFTER_TOOL`              | Return value → rewrites `data.tool_output` (redact/scrub). Return `PluginDecision.deny(reason)` / raise `GuardrailBlocked` → block; the model sees the block reason instead of the tool output. `None` → passthrough. Applies to sync `chat()` and async `achat()`. |
| `before_tool_definitions(defs)`                | `BEFORE_TOOL_DEFINITIONS` | Return a `list` → replaces `data.tool_definitions`                                                                                                                                                                                                                  |
| `before_message(msg)`                          | `MESSAGE_RECEIVED`        | Payload carries `content` + identity (`platform`, `sender_id`, `channel_id`, `channel_type`, `message_id`, `session_id` when set). Return `{"content": "..."}` → rewrites `data.content`. Return `PluginDecision.deny(reason)` → drops the inbound message.         |
| `after_message(msg)`                           | `MESSAGE_SENDING`         | Same enriched payload. Return `{"content": "..."}` → rewrites `data.content`.                                                                                                                                                                                       |
| `message_sent(msg)`                            | `MESSAGE_SENT`            | Observational (successful delivery). Payload carries the same enriched fields; return value is ignored.                                                                                                                                                             |
| `message_undelivered(msg)`                     | `MESSAGE_UNDELIVERED`     | Observational (permanent delivery failure). Payload adds `error` and `notice_delivered` to the enriched fields; return value is ignored.                                                                                                                            |
| `on_permission_ask(target, reason)`            | `ON_PERMISSION_ASK`       | `True`/`False`/`None` → allow/deny/allow                                                                                                                                                                                                                            |
| `on_config(config)`                            | `ON_CONFIG`               | Return a `dict` → rewrites `data.config`                                                                                                                                                                                                                            |
| `on_auth(auth_type, credentials)`              | `ON_AUTH`                 | Return a `dict` → rewrites `data.credentials` (works when starting as `None`)                                                                                                                                                                                       |
| `session_start(context)`                       | `SESSION_START`           | Observational (session `source`, `session_name`, `session_id`)                                                                                                                                                                                                      |
| `session_end(context)`                         | `SESSION_END`             | Observational (`reason`, `total_turns`, `total_tokens`, `session_id`)                                                                                                                                                                                               |
| `on_error(error_type, error_message, context)` | `ON_ERROR`                | Observational (`stack_trace`, nested `context`, `session_id`)                                                                                                                                                                                                       |
| `cli_backend_execute(context)`                 | `CLI_BACKEND_EXECUTE`     | Observational (`backend`, `command` — redacted, `content`, `error`, `transport`, `praisonai_llm_http`, `session_id`)                                                                                                                                                |

`on_config` and `on_auth` write their returned dict back onto the payload even when the target attribute starts as `None` — so a plugin can inject credentials the first time they're requested, not only edit an existing dict.

<Note>
  **`after_tool` is now a redaction / block seam, not an observer** (PraisonAI PRs [#3968](https://github.com/MervinPraison/PraisonAI/issues/3968) / [#3969](https://github.com/MervinPraison/PraisonAI/pull/3969)). A `Plugin.after_tool` return value is written back to the effective tool result before it reaches the model — mirroring `before_tool` (arg rewrite) and `after_llm` (response rewrite). Return a scrubbed value to redact, `PluginDecision.deny("reason")` to suppress, or raise `GuardrailBlocked("reason")` for the same effect via the exception form. See [Redact / Block Tool Output](/docs/features/redact-tool-output).
</Note>

***

## Reference Plugins in `praisonai-plugins`

Batteries-included plugins ship in the separate `praisonai-plugins` package — install once, then enable by name.

| Plugin               | Hook                  | Enable                                                                      |
| -------------------- | --------------------- | --------------------------------------------------------------------------- |
| `cli_backend_tracer` | `cli_backend_execute` | `PRAISONAI_CLI_BACKEND_DEBUG=1 praisonai plugins enable cli_backend_tracer` |

`cli_backend_tracer` overrides `Plugin.cli_backend_execute(context)` and logs every CLI backend delegation to the standard `praisonai` logger — with the prompt already redacted, so the log stream is safe to ship to an aggregator.

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
pip install praisonai-plugins
export PRAISONAI_CLI_BACKEND_DEBUG=1
praisonai plugins enable cli_backend_tracer
```

See [Hook Events → CLI Backend Events](/docs/docs/features/hook-events#cli-backend-events) for the full `CliBackendExecuteInput` payload reference.

***

## Blocking Plugins (Guardrails & Policies)

Guardrail and policy plugins can now stop an action before it happens, not just rewrite it — refuse a dangerous tool call, drop a spam message, or decline an LLM request.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Blocking Plugin"
        Request[📥 Action] --> Guard{🛡️ Guardrail}
        Guard -->|allow / rewrite| Run[✅ Execute]
        Guard -->|deny / block| Stop[🛑 Skipped]
    end

    classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef guard fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef stop fill:#6366F1,stroke:#7C90A0,color:#fff

    class Request input
    class Guard guard
    class Run ok
    class Stop stop
```

### Quick Start

<Steps>
  <Step title="Block a tool call">
    Return `PluginDecision.block(reason)` from `before_tool` to skip a forbidden tool.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, plugins
    from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook, PluginDecision

    class BlockDeleteFiles(Plugin):
        @property
        def info(self):
            return PluginInfo(
                name="block_delete_files",
                hooks=[PluginHook.BEFORE_TOOL],
            )

        def before_tool(self, tool_name, args):
            if tool_name == "delete_file":
                return PluginDecision.block("File deletion is not permitted")
            return args

    plugins.get_plugin_manager().register(BlockDeleteFiles())
    plugins.enable()

    agent = Agent(name="Assistant", instructions="Help users.", tools=["delete_file", "read_file"])
    agent.start("Delete /etc/hosts")   # tool call skipped; block reason surfaced
    ```
  </Step>

  <Step title="Refuse an LLM request">
    Return `PluginDecision.deny(reason)` from `before_llm` and the agent returns `"[LLM request blocked by hook: <reason>]"` without calling the model.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, plugins
    from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook, PluginDecision

    class BlockLLM(Plugin):
        @property
        def info(self):
            return PluginInfo(
                name="block_llm",
                hooks=[PluginHook.BEFORE_LLM],
            )

        def before_llm(self, messages, params):
            return PluginDecision.deny("LLM request refused by policy")

    plugins.get_plugin_manager().register(BlockLLM())
    plugins.enable()

    agent = Agent(name="Assistant", instructions="Help users.")
    print(agent.start("Hello"))   # [LLM request blocked by hook: LLM request refused by policy]
    ```
  </Step>

  <Step title="Drop an inbound message">
    Return `PluginDecision.deny(reason)` from `before_message` to drop a message before the agent sees it.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, plugins
    from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook, PluginDecision

    class BlockSpam(Plugin):
        @property
        def info(self):
            return PluginInfo(
                name="block_spam",
                hooks=[PluginHook.MESSAGE_RECEIVED],
            )

        def before_message(self, message):
            if "spam" in message.get("content", ""):
                return PluginDecision.deny("Spam blocked")
            return message

    plugins.get_plugin_manager().register(BlockSpam())
    plugins.enable()

    agent = Agent(name="Assistant", instructions="Help users.")
    agent.start("buy spam now")   # message dropped before the agent runs
    ```
  </Step>
</Steps>

### Three Ways to Block

The same block, written three ways — pick the one that reads cleanest in your code.

| Style              | Return / raise                                          | When to prefer                                           |
| ------------------ | ------------------------------------------------------- | -------------------------------------------------------- |
| `PluginDecision`   | `return PluginDecision.deny(reason)` / `.block(reason)` | Default. One import, reads like data.                    |
| `HookResult`       | `return HookResult.deny(reason)` / `.block(reason)`     | You already work with `HookResult` elsewhere.            |
| `GuardrailBlocked` | `raise GuardrailBlocked(reason)`                        | Deep in a validator that would otherwise `raise` anyway. |

<CodeGroup>
  ```python PluginDecision theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook, PluginDecision

  class BlockDelete(Plugin):
      @property
      def info(self):
          return PluginInfo(name="block_delete", hooks=[PluginHook.BEFORE_TOOL])

      def before_tool(self, tool_name, args):
          if tool_name == "delete_file":
              return PluginDecision.block("File deletion not permitted")
          return args
  ```

  ```python HookResult theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook
  from praisonaiagents.hooks.types import HookResult

  class BlockDelete(Plugin):
      @property
      def info(self):
          return PluginInfo(name="block_delete", hooks=[PluginHook.BEFORE_TOOL])

      def before_tool(self, tool_name, args):
          if tool_name == "delete_file":
              return HookResult.deny("Denied via HookResult")
          return args
  ```

  ```python GuardrailBlocked theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents.plugins import Plugin, PluginInfo, PluginHook, GuardrailBlocked

  class BlockDelete(Plugin):
      @property
      def info(self):
          return PluginInfo(name="block_delete", hooks=[PluginHook.BEFORE_TOOL])

      def before_tool(self, tool_name, args):
          if tool_name == "delete_file":
              raise GuardrailBlocked("File deletion not permitted")
          return args
  ```
</CodeGroup>

`PluginDecision.deny(reason)` and `PluginDecision.block(reason)` both stop the action (`is_denied()` is `True` for each); `allow(reason=None)` is an explicit no-op. `GuardrailBlocked(reason: str = "Blocked by guardrail plugin")` is caught by the bridge and converted to a block. It may be raised from any `before_*` method **and from `after_tool`** — the tool has already run there, but the output is suppressed before it reaches the model (see [Redact / Block Tool Output](/docs/features/redact-tool-output)).

### Where Blocks Fire

The five `before_*` methods and `after_tool` can block; the remaining `after_*` / observational methods cannot.

| Plugin method             | Can rewrite? | Can block? | Runtime effect of block                                                                                                           |
| ------------------------- | :----------: | :--------: | --------------------------------------------------------------------------------------------------------------------------------- |
| `before_agent`            |       ✅      |      ✅     | Agent run aborts before the first LLM call                                                                                        |
| `before_llm`              |       ✅      |      ✅     | Model call skipped; agent returns `"[LLM request blocked by hook: <reason>]"`                                                     |
| `before_tool_definitions` |       ✅      |      ✅     | Tool surface dropped for this turn (LLM sees no tools)                                                                            |
| `before_tool`             |       ✅      |      ✅     | Tool call skipped; reason surfaced                                                                                                |
| `before_message`          |       ✅      |      ✅     | Inbound message dropped                                                                                                           |
| `after_tool`              |       ✅      |      ✅     | Tool output rewritten/redacted, or suppressed — the model sees the block reason instead ([details](/docs/features/redact-tool-output)) |
| other `after_*`           |       ✅      |      ❌     | *(observational — no block path)*                                                                                                 |

### How a Block Flows

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant Plugin as Guardrail Plugin
    participant Runner as HookRunner
    participant Target as LLM/Tool

    User->>Agent: request
    Agent->>Runner: fire BEFORE_TOOL
    Runner->>Plugin: before_tool(name, args)
    alt allow / rewrite
        Plugin-->>Runner: dict (new args) OR None
        Runner-->>Agent: HookResult.allow()
        Agent->>Target: execute
        Target-->>Agent: result
    else deny / block
        Plugin-->>Runner: PluginDecision.deny(reason) / raise GuardrailBlocked
        Runner-->>Agent: HookResult.deny(reason) — is_blocked=True
        Agent-->>User: blocked (reason surfaced)
    end
```

### Which Style Should I Use?

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start[🛡️ Need to block?] --> Q1{Inside a validator<br/>that already raises?}
    Q1 -->|Yes| Raise[raise GuardrailBlocked]
    Q1 -->|No| Q2{Already import<br/>HookResult?}
    Q2 -->|Yes| HR[return HookResult.deny]
    Q2 -->|No| PD[return PluginDecision.deny]

    classDef start fill:#8B0000,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,Q2 question
    class Raise,HR,PD answer
```

***

## Ship a Plugin as a pip Package

Register your plugin class in the `praisonai.plugins` entry-point group in `pyproject.toml`:

```toml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
[project.entry-points."praisonai.plugins"]
my_plugin = "my_package.plugins:MyPlugin"
```

Agent construction (or `plugins.enable()`) auto-discovers and bridges it — no user code changes needed.

Once installed, verify the plugin registered with `praisonai plugins add <package> --dry-run` — see [plugins add](/docs/features/plugins-add).

<Note>
  **A shipped plugin cannot replace a built-in name.** PraisonAI's registries subclass the base `PluginRegistry`, so since [PR #4176](https://github.com/MervinPraison/PraisonAI/pull/4176) any entry point whose name (case-insensitive) matches a shipped built-in is skipped and the built-in is kept (a `DEBUG` line notes the collision). Pick a name unique to your package. Runtime `register(...)` is the deliberate override path. See [Plugin Precedence](/docs/features/plugin-precedence).
</Note>

***

## CLI Commands

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai plugins list
praisonai plugins enable pii_guardrail
praisonai plugins disable pii_guardrail
praisonai plugins reload
praisonai plugins doctor
praisonai plugins add praisonai-my-plugin
praisonai plugins add praisonai-my-plugin --dry-run
```

`praisonai plugins list` shows every plugin with its `source` (`entry_point:<dist>`, `registered`, or `single_file`). `enable`/`disable` persist to the config the runtime reads, `reload` picks up edits without a restart, and `doctor` diagnoses issues. See the full reference on the [Plugins CLI](/docs/cli/plugins) page.

***

## Configuration Options

Only `Plugin Name` is required; every other field is optional.

| Field                                     | Required | Type   | Description                                               |
| ----------------------------------------- | -------- | ------ | --------------------------------------------------------- |
| `Plugin Name`                             | Yes      | string | Display name in plugin header                             |
| `Description`                             | No       | string | What the plugin does                                      |
| `Version`                                 | No       | string | Semantic version (defaults to `1.0.0`)                    |
| `Author`                                  | No       | string | Plugin author                                             |
| `Hooks`                                   | No       | list   | Hook events this plugin registers                         |
| `Dependencies` (or `Deps`)                | No       | list   | Python packages the plugin needs                          |
| `Channels`                                | No       | list   | Channels/adapters this plugin contributes                 |
| `Provides` (or `Provides Tools`, `Tools`) | No       | list   | Tool/function names this plugin exposes                   |
| `Config` (or `Config Schema`)             | No       | list   | Config keys this plugin reads                             |
| `Auto Enable When Configured`             | No       | list   | Env/credential keys that auto-enable this plugin when set |

***

## Best Practices

<AccordionGroup>
  <Accordion title="Keep plugins single-purpose">
    One file per concern — weather tools, logging, or guardrails, not all three.
  </Accordion>

  <Accordion title="Call discover_and_load_plugins() once">
    Load before creating the agent so tools and hooks register globally.
  </Accordion>

  <Accordion title="Reference tools by name">
    Pass `tools=["get_weather"]` — the string must match the `@tool` function name.
  </Accordion>

  <Accordion title="Use plugins.enable for built-ins">
    Enable `logging` and `metrics` without writing plugin files.
  </Accordion>

  <Accordion title="Call plugins.enable() to activate lifecycle methods">
    Tools and guardrails work without `plugins.enable()`. But lifecycle-method plugins (subclasses of `Plugin` that override `before_llm`, `after_llm`, and so on) only fire after they are wired into the runtime hook registry. Call `plugins.enable()` explicitly, or set `PRAISONAI_PLUGINS=true` / `[plugins] enabled = true` and Agent construction wires them automatically.
  </Accordion>

  <Accordion title="Reach for PluginDecision before HookResult">
    `PluginDecision.deny(reason)` reads as one line and only needs the plugin import. Use `HookResult.deny(reason)` only when you already import it for another reason, and `raise GuardrailBlocked(reason)` only when you're inside a validator that already raises on failure. Prefer `block` over `deny` when the action is categorically forbidden; use `deny` when it's contextually refused (this input, this user, this time).
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Hooks" icon="webhook" href="/docs/features/hooks">
    Hook events and the HookRegistry API
  </Card>

  <Card title="Toolsets" icon="wrench" href="/docs/features/toolsets">
    Create and register custom agent tools
  </Card>

  <Card title="Plugins CLI" icon="plug" href="/docs/cli/plugins">
    List, enable, disable, reload, and diagnose plugins
  </Card>

  <Card title="Config File" icon="file-code" href="/docs/features/config-file">
    Turn plugins on from `[plugins]` in `config.toml`
  </Card>

  <Card title="Tool Discovery" icon="list-tree" href="/docs/features/tool-discovery-order">
    How Agent resolves tool names at runtime
  </Card>

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

  <Card title="Redact Tool Output" icon="shield-halved" href="/docs/features/redact-tool-output">
    Scrub secrets or block tool results before the model sees them
  </Card>
</CardGroup>
