The Incidents That Made Me Build Guardrails I Should Have Built First
Published: April 21, 2026 · 8 min read
I build AI automation systems for small business clients. That means agents running on their machines, watching their inboxes, reading their messages, and sending on their behalf. The upside is real: hours returned, faster response times, better follow-through on things that used to fall through the cracks.
The downside, when something goes wrong, is that it goes wrong in front of the client's clients. Not in a log file. Not in a test environment. In the real world, with real people on the receiving end.
Two incidents from March 2026 taught me the rules I should have built in from the start. I'm documenting them in full because the specifics are where the lesson lives.
Incident 1: 78 CMA PDFs With Blank Addresses and Fake Estimates
I built an email automation for a real estate agent — call him Adam. The system would watch his Gmail for emails with "CMA:" in the subject line, extract the property address, generate a Comparative Market Analysis PDF, and send it to the relevant contact. The intent was to take a manual, repetitive task and remove it from his plate entirely.
The Gmail query I wrote was: subject:"CMA:".
That query matches any email with "CMA:" in the subject. Emails Adam sent. Emails Adam received. Emails from other agents, from listing services, from anyone whose message happened to contain that string. I did not add from:me. I did not filter to outbound-only. The system watched the entire mailbox and treated every match as a trigger.
The second problem: I did not validate inputs before running the pipeline. If the property address field was empty — because the email came from somewhere the parser didn't expect, or because the format was slightly off — the system did not abort. It ran the report anyway. With a blank address. With comps pulled against nothing. The output was a professionally formatted PDF with blank fields and fabricated estimates in the $300,000 range.
Seventy-eight of those PDFs were sent to Adam's clients.
The third problem came after the fact. When I checked whether anything had been sent, the result came back clean — no emails sent, system nominal. That was wrong. The machine's keychain had auto-locked during an SSH session. The token read failed silently. My code interpreted a credential failure as a clean execution result and reported accordingly. I told my client nothing had gone out. Everything had gone out.
Three failures, stacked: a filter that was too broad, no input validation, and a false negative that came from confusing a credential error for an execution result.
Incident 2: An AI Agent Responding to Real Estate Leads Nobody Asked It To Contact
A different client had an AI agent configured with allowFrom: * on the iMessage channel.
What that setting does: it tells the agent to respond to any incoming iMessage from any sender. There is no allowlist. No filtering by number. No restriction to known contacts. Any message that arrives gets treated as something the agent should respond to.
What I had not fully accounted for: iMessage on macOS reads the device's entire Messages database. Not a dedicated inbox. Not filtered threads. Everything. Years of conversations, real estate leads, buyer and seller contacts, personal messages, all of it was visible to the agent as potential input.
The agent started responding to the client's real estate leads. Twenty or more of them, over a period of time, before I caught it. The client didn't know it was happening. The leads didn't know they were talking to an AI. Nobody had asked the agent to reach out to any of them. It just saw messages arriving and did what it was configured to do: respond.
allowFrom: * means exactly what it says. I had treated it as a convenience setting. It was an exposure setting, and I had left it open on a channel connected to a full message history.
The Rules That Came From These Two Incidents
I have four hard rules now that I didn't have before. They apply to every automation I build that sends or responds on a client's behalf.
Any automation that sends requires the tightest possible filter. For Gmail: from:me is not optional on outbound-triggered pipelines, it's the first condition in the query. For iMessage: an explicit allowlist of phone numbers, never a wildcard. allowFrom: * is not a default I'll ever ship on a production channel again. The worst-case scenario for a filter that's too tight is that some messages don't get processed. The worst-case scenario for a filter that's too broad is what happened above.
Validate inputs before running. If the piece of data that makes an action meaningful — an address, a name, an amount, a recipient — is empty or malformed, the correct behavior is to abort and log, not to proceed with partial data. A graceful abort with a log entry is a recoverable outcome. A sent document with blank fields is not.
Never report "nothing sent" without confirming from the actual service's delivery records. A credential failure is not the same as a clean run. If the system can't read the token, it cannot confirm what happened. The correct report in that case is "could not verify" — not "nothing sent." These are different statements with different implications, and conflating them is how false confidence gets passed to clients.
iMessage is uniquely dangerous and must be treated accordingly. It reads the entire native Messages database on the device. It has no concept of a filtered inbox. Any agent connected to iMessage has access to years of conversations by default. The appropriate configuration is a tight allowlist containing only the numbers the agent is explicitly authorized to interact with. Anything broader than that is an incident waiting to happen.
Why I'm Publishing This
The standard version of a "building in public" post is about wins. Workflows that worked, clients that scaled, systems that ran cleanly. Those are real and I have them.
But the incidents are where the durable knowledge actually lives. The CMA disaster and the iMessage situation didn't come from recklessness — they came from moving fast and trusting the happy path. The filter was almost right. The iMessage channel would have been fine in most contexts. Almost and most are where things go wrong in production.
The guardrails I built after these incidents are not clever. They're not sophisticated architecture. They're just the checks that should have been there from the start: tightest possible filter, validate before running, don't confuse a read failure for a clean result, treat iMessage like the full message database it is.
If you're building automations that act on behalf of clients — sending emails, responding to messages, generating and distributing documents — run those four rules against every pipeline you have right now. The cost of the check is about ten minutes. The cost of skipping it showed up in 78 PDFs and 20 unexpected conversations.
— Deacon Ridley, April 2026