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

# Outbound Secret Scrub

> Never deliver a registered secret or a credential-shaped token to a chat user

Every reply, streamed draft, and proactive send is passed through a scrubber before it leaves the process — on by default, no config.

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

# No configuration — every outbound reply is scrubbed by default.
agent = Agent(name="Ops", instructions="Answer questions.")
bot = TelegramBot(token="...", agent=agent)

import asyncio
asyncio.run(bot.start())
# A tool result containing sk-abc… or AKIA… is masked as [REDACTED]
# before the reply is sent to Telegram.
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Outbound Reply Path"
        L[🤖 LLM / Tool Output] --> H[🪝 MESSAGE_SENDING Hook]
        H --> S[🛡️ Outbound Scrub]
        S --> W[📨 Chat Platform]
    end

    classDef llm fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef hook fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef scrub fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef out fill:#10B981,stroke:#7C90A0,color:#fff

    class L llm
    class H hook
    class S scrub
    class W out
```

## Quick Start

**1. Zero config — scrubbing is on by default**

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

agent = Agent(name="Ops", instructions="Answer questions.")
bot = TelegramBot(token="...", agent=agent)

import asyncio
asyncio.run(bot.start())
```

Registered gateway credentials and credential-shaped tokens are masked in every reply without any setup.

**2. Opt out per bot**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
bot._redact_secrets_outbound = False
```

Disable the scrub for a single bot. Only do this when another layer upstream already guarantees no secret leaves.

**3. Bring your own redactor (e.g. add PII)**

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

class PIIRedactor:
    def redact(self, text: str) -> str:
        text = redact_outbound(text)          # keep the built-in secret scrub
        return text.replace("@example.com", "[EMAIL]")  # add your own policy

bot._outbound_redactor = PIIRedactor()
```

Inject a custom scrubber that wraps the core primitive and adds extra rules.

***

## What Gets Masked

Two things are masked before a reply leaves the process: every value registered via `register_secret_for_redaction` (all resolved gateway credentials, masked by exact value) **plus** any token matching a narrow credential-shape pattern.

| Shape | Example match |
| - | - |
| OpenAI / Anthropic keys | `sk-…`, `sk-proj-…`, `sk-ant-…` |
| AWS access key id | `AKIA` + 16 upper/digit |
| GitHub tokens | `ghp_…`, `gho_…`, `ghu_…`, `ghs_…`, `ghr_…` |
| GitHub fine-grained PAT | `github_pat_…` |
| Google API key | `AIza` + 35 chars |
| Slack tokens | `xoxb-…`, `xoxa-…`, `xoxp-…`, `xoxr-…`, `xoxs-…` |
| Authorization bearer | `Bearer <16+ chars>` |
| PEM private key | `-----BEGIN … PRIVATE KEY----- … -----END … PRIVATE KEY-----` |

> [!NOTE]
>
> Personal data (PII) is deliberately out of scope for the built-in scrub — the shape patterns stay narrow so ordinary text is never over-redacted. Layer PII on with a custom redactor (see below).

***

## How It Works

The scrub runs **after** every `MESSAGE_SENDING` hook, so a hook cannot re-introduce a secret after your own logic runs.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Hook as MESSAGE_SENDING Hook
    participant Scrub as Outbound Scrub
    participant Adapter
    participant User

    Agent->>Hook: reply text
    Hook-->>Scrub: text (possibly rewritten)
    Scrub->>Scrub: mask registered + shaped secrets
    Scrub->>Adapter: scrubbed text
    Adapter-->>User: delivered reply
```

***

## Which Paths Are Covered

Every path that dispatches text to a chat user is scrubbed.

| Path | Method | When it fires |
| - | - | - |
| Reply | `fire_message_sending` | Every direct reply an adapter dispatches |
| Streaming | `DraftStreamer._render_content` + `.finalize` | Every intermediate draft edit and the final answer |
| Proactive / scheduled | `DeliveryRouter.deliver` | `agent.send_message(...)`, continuables, scheduled sends |
| Relay | `RelayAdapter.send_message` | Relay-backed adapter replies |

> [!NOTE]
>
> The reply seam runs the scrub **last** — after any `MESSAGE_SENDING` hook. Streaming edits bypass the reply seam, so both the rolling draft and the final answer are scrubbed in the streamer, before text-limit truncation, so a `[REDACTED]` mask is never cut across the length boundary.

***

## Custom Redactor

Inject an `OutboundRedactor` to add a policy the core scrub does not cover — PII, a company-internal token pattern, and so on.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.secrets import redact_outbound, OutboundRedactor

class MyRedactor:
    def redact(self, text: str) -> str:
        text = redact_outbound(text)                 # keep built-in secret scrub
        return text.replace("INTERNAL-", "[REDACTED]")  # your extra rule

bot._outbound_redactor = MyRedactor()
```

`OutboundRedactor` is a `@runtime_checkable` protocol with one method, `redact(self, text: str) -> str`. The injected redactor wins over the core primitive. It must return a `str` — any non-`str` return is ignored and the core `redact_outbound` runs instead.

***

## Opt-Out

Turn the scrub off for a single bot by setting one attribute.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
bot._redact_secrets_outbound = False
```

> [!WARNING]
>
> Only disable the outbound scrub if another guarantee upstream already owns secret redaction (for example, a PII policy that also masks credentials). Otherwise resolved secrets and credential-shaped tokens will be delivered verbatim.

***

## Ordinary Text Is Unchanged

The scrub is additive — absent a leak, ordinary text passes through untouched.

| Original | Delivered |
| - | - |
| "Standup is at 9:30am." | "Standup is at 9:30am." |
| "Call me on 555-0134." | "Call me on 555-0134." |
| "Meet in room B-204." | "Meet in room B-204." |
| "Your key is sk-abc123def456ghi789." | "Your key is \[REDACTED]." |

***

## Idempotency & Failure Modes

Scrubbing is idempotent — running it on already-scrubbed text is a no-op, so overlapping seams (finalize + reply hook) never double-mask. It is also best-effort: a scrubber exception is caught at every seam and the original text is delivered rather than dropped, so a bug in a custom redactor can never block delivery.

***

## Relationship to the Inbound Gate

Inbound and outbound are symmetric. The [Inbound Message Gate](https://praison.ai/docs/features/inbound-message-gate.md) filters what reaches the agent through `MESSAGE_RECEIVED`; the outbound scrub filters what leaves the agent on the reply path. Together they bracket the agent on both sides.

> **[Inbound Message Gate](https://praison.ai/docs/features/inbound-message-gate.md)**
>
> Drop or redact incoming messages before the agent sees them.

***

> [!NOTE]
>
> For scrubbing inbound text and traces (as opposed to outbound chat replies), see [Security & Redaction](https://praison.ai/docs/features/context-security-redaction.md), which also masks credentials embedded in URLs.

## Best Practices

> **Leave it on**
>
> The scrub is safe-by-default and additive — ordinary text is unchanged. Keep it on and opt out only when another layer already owns secret redaction.

> **Register any custom credential shape**
>
> The shape regex is deliberately narrow. For an unusual token format, register the value with `register_secret_for_redaction(value)` so it is masked by exact value even when the shape patterns miss it.

> **Layer PII on top with a custom OutboundRedactor**
>
> Don't disable the core scrub to add PII masking — wrap it. Call `redact_outbound(text)` first inside your `redact` method, then apply your own rules.

> **Prefer scrubbing at the source for high-volume tool output**
>
> For a tool that emits large or frequent output, scrub at the tool boundary with a guardrail — cheaper than scrubbing every reply. See [Redact Tool Output](https://praison.ai/docs/features/redact-tool-output.md).

***

## Related

> **[Inbound Message Gate](https://praison.ai/docs/features/inbound-message-gate.md)**
>
> The symmetric inbound filter on `MESSAGE_RECEIVED`.

> **[Gateway Secret References](https://praison.ai/docs/features/gateway-secret-references.md)**
>
> Load credentials from files, env vars, or secret managers.

> **[Secret Egress Sentinels](https://praison.ai/docs/features/secret-egress-sentinels.md)**
>
> The before-the-fact counterpart: reference a secret by an opaque token so plaintext never reaches the model.

> **[Hook Events](https://praison.ai/docs/features/hook-events.md)**
>
> `MESSAGE_SENDING` and the full hook event reference.

> **[Redact Tool Output](https://praison.ai/docs/features/redact-tool-output.md)**
>
> Scrub secrets at the tool boundary before they reach a reply.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.