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

Outbound Reply Path

🤖 LLM / Tool Output

🪝 MESSAGE_SENDING Hook

🛡️ Outbound Scrub

📨 Chat Platform

Quick Start

  1. Zero config — scrubbing is on by default
    python
    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
    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
    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.

ShapeExample match
OpenAI / Anthropic keyssk-…, sk-proj-…, sk-ant-…
AWS access key idAKIA + 16 upper/digit
GitHub tokensghp_…, gho_…, ghu_…, ghs_…, ghr_…
GitHub fine-grained PATgithub_pat_…
Google API keyAIza + 35 chars
Slack tokensxoxb-…, xoxa-…, xoxp-…, xoxr-…, xoxs-…
Authorization bearerBearer <16+ chars>
PEM private key-----BEGIN … PRIVATE KEY----- … -----END … PRIVATE KEY-----

How It Works

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

Diagram
UserAdapterOutbound ScrubMESSAGE_SENDING HookAgentUserAdapterOutbound ScrubMESSAGE_SENDING HookAgentreply texttext (possibly rewritten)mask registered + shaped secretsscrubbed textdelivered reply

Which Paths Are Covered

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

PathMethodWhen it fires
Replyfire_message_sendingEvery direct reply an adapter dispatches
StreamingDraftStreamer._render_content + .finalizeEvery intermediate draft edit and the final answer
Proactive / scheduledDeliveryRouter.deliveragent.send_message(...), continuables, scheduled sends
RelayRelayAdapter.send_messageRelay-backed adapter replies

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
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
bot._redact_secrets_outbound = False

Ordinary Text Is Unchanged

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

OriginalDelivered
“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 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.


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.


Related