Content scanning

Every message ReplyLayer handles is passed through a security scan before it reaches its destination — inbound mail is scanned before your agent can read it, and your agent's outbound mail is scanned before it goes on the wire. The scan is an LLM-backed evaluation of the message content plus deterministic pattern checks. It runs once, at the point the message is processed, and the result is persisted on the message so you can read it back later; it is not re-run on every request.

This guide covers what the scan looks for in each direction, the difference between a quarantined message (held, but releasable) and a blocked one (terminal), and — importantly — what actually happens when you release held mail.

Scanning is directional

ReplyLayer scans in both directions, but the two scans look for different things. The asymmetry is deliberate: each direction faces a different threat.

Inbound — mail arriving for your agent is untrusted input. The inbound scan looks for attacks on the agent (prompt-injection, jailbreak, and function-call risk) and for the tone or nature of the content (six advisory criteria such as toxicity or self-harm). The inbound scan does not run the secrets scanner: a sender who pastes a credential into an email to your agent is the sender's own data arriving in your inbox, not your platform leaking a secret outward, so it is delivered as ordinary content.

Outbound — mail your agent sends is scanned for exfiltration. The outbound scan runs a secrets scanner that looks for credentials leaving the platform — cloud access keys, API tokens, private keys, and generic bearer tokens — and a real or planted secret in an outbound message is blocked. This scanner runs outbound-only. Your agent's own outbound text that happens to resemble prompt-injection is content your agent authored, not untrusted input, so it is not quarantined the way inbound prompt-injection is.

In each case the control protects the direction where the threat actually lives: exfiltration on the way out, untrusted-input attacks on the way in.

Bodies are untrusted — the scan is a filter, not a sanitizer

ReplyLayer scans inbound content; it does not strip the instructions out of the body your agent reads. A clean verdict means "no attack pattern was detected," not "this text is safe to follow." Every inbound message read carries an agent_safety_context object marking the body as untrusted, with stable handling guidance your integration can render or log verbatim. Treat every inbound body as data, never as instructions — do not let a body redirect your agent, reveal its system prompt, trigger tool calls, or fetch URLs it names. For the full per-field trust model and how agent_safety_context is populated, see the agent security model.

Detection is defense-in-depth, not a wall. Known residuals to design around:

  • Obfuscated or semantic-substitution injections (for example a URL written hzzps://… with a "fix the typo" instruction) can slip past pattern matching.
  • Plaintext comment syntax (<!-- … --> outside an HTML part) is delivered verbatim; only HTML-part comments are stripped.
  • Instructions split across several messages in a thread, each individually benign, are not correlated: a message is scanned in isolation.

Your agent treating body text as data is the backstop for all three.

The one narrow exception to "treat the body as untrusted" is trusted instruction sources: a customer can designate a specific verified sender address so reads from that sender carry relaxed handling guidance. That relaxation is turned on only from a re-authenticated dashboard session — it is never something an agent or API key can enable for itself — and it never changes what your agent is allowed to send.

Quarantine versus block: releasable versus terminal

The scan attaches a rollup verdict to each message. scan.verdict reports the scanner's own decision and ranges from clean through warning, review_required, quarantined, and blocked — with quarantined being a releasable, non-terminal decision and blocked being terminal. scan.verdict is the scanner-decision channel and is not guaranteed to equal a message's delivery state. For the full verdict vocabulary and how it relates to the message state machine, see the messages contract.

Inbound attack criteria → quarantine

Three inbound criteria are treated as attacks on your agent. A flag on any of them quarantines the message so the agent never reads it in the normal flow:

CriterionWhat it catches
prompt_injectionEmbedded instructions trying to steer the agent
jailbreakAttempts to disable the agent's safety controls
function_call_riskContent engineered to trigger unsafe tool or function calls

A quarantined message is held, not deleted. Someone with the right access can release it — see Releasing held mail below.

Outbound secrets → block

An outbound message the secrets scanner flags is blocked before it ships. A blocked outbound message is terminal for that send; the fix is to remove the flagged content and send again (which re-scans — see below), not to release it.

Advisory findings — delivered, with a warning

Six inbound criteria describe the tone or nature of the incoming content rather than an attack. When one fires, the decision is allow_with_warning: the email is still delivered to your agent, and a finding is attached describing what was flagged.

CriterionWhat it catches
toxicityHarmful or abusive content
harassmentTargeted hostile language
violenceReferences to physical harm or threats
sexual_contentSexually explicit material
profanityOffensive language
self_harmReferences to self-harm or suicide

Advisory findings exist so a hostile or distressing email does not silently pass through your agent unmarked — the agent is told before it drafts a reply. If you ignore them nothing breaks and the email is still delivered; the signal is there specifically so your agent can avoid mirroring the flagged tone and can escalate the serious cases (self_harm, violence, harassment) to a human instead of auto-responding.

Reading a finding

Advisory findings are read back from the scan object on the message (GET /v1/messages/:id, the list, thread, and draft reads, and the SDK methods that wrap them). All six advisory criteria collapse into a single harmful_content category — inspect the subtype field to tell them apart:

{
  "state": "available",
  "scan": {
    "verdict": "warning",
    "categories": [
      { "category": "harmful_content", "decision": "allow_with_warning" }
    ],
    "findings": [
      {
        "category": "harmful_content",
        "subtype": "harassment",
        "decision": "allow_with_warning",
        "reason": "Content flagged for safety review",
        "failure_class": "model_judgment"
      }
    ]
  }
}

Note the message state is available (delivered and readable), not quarantined. A message with no finding returns "scan": { "verdict": "clean", "categories": [], "findings": [] }.

There is no inbound-arrival webhook for advisory findings — to consume one, your agent reads the message. (See webhooks for the events that do fire.)

failure_class — a judgment, or a scan that failed?

Each finding may carry an optional failure_class:

  • model_judgment — the model evaluated the content and reached this verdict. It is a real content finding.
  • inference_error — a scanner failed for an infrastructure reason, not because the content was harmful. Treat it as "scan incomplete," apply your own caution policy, and do not treat it as a confirmed content hit.

This distinction is deliberate honesty: an infrastructure failure is reported as such rather than dressed up as a content decision. Machine-readable handling guidance for a finding is available via the agent_instructions[] field; see the error and instruction reference.

Releasing held mail

Releasing a quarantined message is a state change, not a re-scan — and the behavior differs between a held inbound message and a held outbound draft. Getting this right matters.

Releasing a held inbound message flips its state; it does not re-run the scan. POST /v1/messages/:id/release moves a quarantined inbound message to available and hands it to your agent exactly as it was first scanned. The original finding stays on the record — you are overriding the hold, not clearing the verdict. Nothing is re-evaluated.

A held outbound draft is re-scanned every time you send it. When you send a draft that was held for review, the send path runs an authoritative outbound scan over the exact bytes that will ship. So if you edit a held draft to remove the flagged content and send it again, it gets a fresh verdict: the resend passes if your edit resolved the issue, and is rejected again if it did not. Editing and re-sending a draft is not the same operation as releasing a quarantined message — the draft path re-scans; a message release does not.

(An outbound message that the scanner quarantined can also be released, which dispatches it to the provider using the decision already on record; like an inbound release, that path delivers using the recorded decision, with no fresh scan.)

Per-mailbox scanner policy

Scanning behavior is configured per mailbox. Each mailbox runs on a scanner policy that you can tune from your mailbox settings in the dashboard or the mailbox update API — for example, disabling an individual scanner, choosing how specific outbound PII types (ssn, credit_card, phone_number) are handled (allow, allow_with_warning, review, quarantine, or block), or routing matching sends to human review. A mailbox with no explicit policy uses the platform defaults, and clearing the policy resets it to those defaults. Loosening a policy is an admin or dashboard-session action; agent-role keys cannot weaken a mailbox's scanning.