AgentGuards
← Blog

Our Coding Agent Almost Leaked Every Customer's Email

July 22, 2026· dev@alphaops.devpii-detectionprompt-injectionllm-securityguardrailsowaspdogfooding

Last week someone on our team asked Claude Code, working inside our own repo, to pull up one customer's usage and billing numbers. Nothing exotic — a normal internal support task, the kind every SaaS team does a dozen times a week. The agent called our own production admin API to look up the tenant. The API answered with more than anyone asked for: a full tenant listing that included every other customer's real email address, not just the one record we wanted.

Nobody prompted for that. There was no jailbreak, no malicious payload, no adversarial input anywhere in the loop. It was our own agent, doing exactly what it was told, hitting an endpoint that happened to over-return data. And it's a more common failure mode than most teams admit.

What actually happened

The admin endpoint in question lists tenants. Given a search term it's supposed to narrow, but on this call it came back with the entire tenant table — emails included — instead of the single row the agent needed. That response landed as tool output in the middle of an active Claude Code session, about to become part of the conversation history the model reasons over for the rest of the task.

We run the AgentGuards Claude Code hook on our own machines, same as any customer would. It wires into PostToolUse, so every tool result — file reads, shell output, API responses — gets scanned before the agent is allowed to act on it. That's what fired here. The hook's pii_detection check, which is a regex-based check that matches on the shape of data rather than trying to understand it, flagged an email address pattern in the response:

code
decision: redact
check: pii_detection
severity: high
reason: email address pattern matched in tool output

Claude Code discarded the flagged payload and continued the task using only the one tenant record it already had on hand from earlier in the conversation. The customer lookup finished fine. Nobody's inbox address ended up sitting in a model's context window, a log line, or a screenshot in a support ticket.

Input scanning is half the story

Most conversations about LLM security start and end with prompt injection: attacker crafts a message, model gets tricked, bad things happen. That framing misses a whole other failure class that doesn't need an attacker at all. OWASP's LLM Top 10 for 2025 ranks Sensitive Information Disclosure at #2, precisely because models — and the agents built around them — routinely end up holding data nobody meant to hand them, through completely legitimate channels: an API that returns too much, a log file that wasn't scrubbed, a database dump pulled in for "context."

The companion risk, Excessive Agency (LLM06:2025), is the other half of why this happened at all: an internal endpoint had more functionality — and returned more data — than the task in front of it needed. The fix for that is a scoped, paginated API that only ever returns the tenant you asked for. We're making that change. But APIs get this wrong constantly, in every codebase, and an agent that blindly trusts whatever a tool call returns will happily carry that mistake straight into a model's context, a summary, or a downstream ticket.

Input screening and output screening are different jobs

A guardrail that only checks what the user types will miss this entirely, because the user never typed anything wrong. The thing that needs screening is what comes back from tools — API responses, file contents, fetched web pages — before the agent is allowed to reason over it or act on it.

Regex vs. NER: which check actually caught this

Worth being precise here, because we ship two different PII checks and they're not interchangeable. What fired in this incident was pii_detection, a fast regex-based check that matches structured PII by shape — email addresses, API keys, credit card numbers, phone numbers. It's cheap, deterministic, and reliable for anything with a recognizable pattern, which is exactly what caught this: an email address is a regex-friendly shape.

We also run a separate, optional check called presidio — a self-hosted deployment of Microsoft's Presidio NER engine — for the PII regex genuinely can't catch: names, physical addresses, free-text mentions of a person. That's a different tool solving a different problem, and it wasn't what fired here. If your data is mostly structured (emails, tokens, IDs), the regex check alone catches a lot. If you're worried about someone's name showing up in a support transcript, you want the NER layer too.

Dogfooding, not marketing

We didn't write this post to prove a product works — we run the same hook on our own repo that we ship to customers, and it caught something in our own workflow, unprompted, during a completely ordinary task. That's the actual test of a guardrail: not how it performs against a red-team prompt someone wrote to make a good demo, but whether it's still watching on the Tuesday afternoon when nobody's thinking about security at all.

If you want to see the same class of catch in action, we recorded one: ▶ Watch: Watch AgentGuards Stop a Password Theft Attempt Live on our channel — a different sensitive-data scenario, same idea: the guardrail scans what comes back from a tool call and stops it before it reaches the conversation.

If you're running Claude Code, Codex, Gemini CLI, or Copilot CLI against a codebase that talks to internal APIs, the input side of your threat model is probably fine. Check the output side. Our OWASP Agentic mapping covers where this fits against LLM06 and the sensitive-information-disclosure risks above, and the PII detector tool is a quick way to see what a regex-shape check catches versus what it doesn't. Getting started walks through wiring the hook into an existing agent in a few minutes — it's the same setup we're running on this exact repo.

The tenant list bug is getting fixed at the source, because that's the correct fix. But the reason nobody's email address is sitting in a Claude Code transcript right now is that something was watching the output, not just the input.