# 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](/agents/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](/agents/trusted-instructions): 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](/agents/messages).

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

| Criterion | What it catches |
|---|---|
| `prompt_injection` | Embedded instructions trying to steer the agent |
| `jailbreak` | Attempts to disable the agent's safety controls |
| `function_call_risk` | Content 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](#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.

| Criterion | What it catches |
|---|---|
| `toxicity` | Harmful or abusive content |
| `harassment` | Targeted hostile language |
| `violence` | References to physical harm or threats |
| `sexual_content` | Sexually explicit material |
| `profanity` | Offensive language |
| `self_harm` | References 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:

```json
{
  "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](/docs/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](/agents/errors).

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

## Related

- [Messages and state machine](/agents/messages) — verdict vocabulary, message
  states, releasable versus terminal.
- [Agent security model](/agents/security-model) — the per-field trust taxonomy
  and how `agent_safety_context` is populated.
- [Send gates](/agents/send-gates) — why an outbound send was blocked, held, or
  rejected.
- [Inbound firewall](/docs/guides/inbound-firewall) — controlling which senders
  reach a mailbox before the scan even runs.
- [Do-not-contact list](/docs/guides/suppressions) — the send-time recipient
  gate.
