Shotoku
← Blog

How to stop an AI agent from emailing the wrong people

· By the Shotoku team

Short answer:check every recipient in code before the message is sent. Allow domains you trust, and hold everything else for a human. An email can’t be unsent, so this check has to run before the send, not in a review afterward.

Why does an agent email the wrong person?

A rule in the prompt helps with none of these reliably. The second case is worse: the model is following instructions, just the wrong ones.

How do you check recipients?

Install Shotoku:

bash
npm install -g shotoku-cli
npm install @shotoku/core
shotoku init

Write a policy keyed on the recipient’s domain:

policy.yaml
rules:
  - resource: yourcompany.com
    actions: [send_email]
    verdict: approved

  - resource: "*.yourcompany.com"
    actions: [send_email]
    verdict: approved

  - resource: "*"
    actions: [send_email]
    verdict: pending_approval

defaultVerdict: pending_approval

Then wrap your send function. Check each domain, and send only if all of them pass, so one outside address in the list holds the whole message:

send.ts
import { authorize } from "@shotoku/core";

const options = { policyPath: "./policy.yaml", ledgerPath: "./data/decisions.jsonl" };

export async function sendAgentEmail(to: string[], message: Message) {
  const domains = new Set(to.map((address) => address.split("@").pop()!.toLowerCase()));

  for (const domain of domains) {
    const decision = await authorize(
      { actor: "mail-agent", action: "send_email", resource: domain },
      options,
    );
    if (!decision.approved) {
      throw new Error(`Email held: ${domain} [${decision.decisionId}]`);
    }
  }

  return mailer.send(to, message);
}

Include CC and BCC in to, or the check won’t see those recipients.

What happens when it runs?

We ran this logic on the current npm release of @shotoku/core:

RecipientsResult
a@yourcompany.com and b@eu.yourcompany.comsent
a@yourcompany.com and c@customer.comheld: customer.com needs a human
Same list, after shotoku approvesent
Same list againheld again

What should you watch out for?

What about a Gmail MCP server?

Wrapping an MCP serveralso works, with one difference: a wrapped rule matches the tool name, not the recipient. So you can allow drafts, and hold every send for a human, but you can’t allow one domain through that route.

RouteCan the agent skip it?Rule matches on
authorize() in your send functionNot if it's the only way to sendRecipient domain
Wrapped MCP mail serverNot through MCPTool name

Common questions

Should agents send email without review at all?

Internal notifications to known addresses are usually fine. For anything that reaches a customer, start with human approval, then relax it once your history shows the agent gets it right.

Does the email text leave my machine?

Not through Shotoku. It runs locally and this check sends it only the domain, which is stored in a local file.

Shotoku is built for spend controls, and the same policy engine covers email. See the policy reference.

Related