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:
| 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 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:
{
"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.
Related
- Messages and state machine — verdict vocabulary, message states, releasable versus terminal.
- Agent security model — the per-field trust taxonomy
and how
agent_safety_contextis populated. - Send gates — why an outbound send was blocked, held, or rejected.
- Inbound firewall — controlling which senders reach a mailbox before the scan even runs.
- Do-not-contact list — the send-time recipient gate.