# LLM guardrails: PII and secrets before egress

> LLM guardrails scan prompts for PII and secrets before egress. Block, mask, or log; where the policy attaches; and who in your organization owns each choice.

- Canonical: https://secondstack.ai/blog/llm-guardrails/
- Published: 2026-08-26
- Author: Oxana Astapova

---

An engineer pastes a config file into a chat with a model, asking why the service will not start. The answer comes back correctly, referencing the connection string with the password intact. What the provider received was not the password. It was a placeholder, swapped in on the way out and swapped back on the way in, and the engineer's screen never showed the difference.

That is the version worth building toward, and it is not where most rollouts start. They start with a policy that says do not paste credentials into AI tools, which no one can enforce, and then with a blocking filter people learn to route around within a month. The decisions that matter are in between: what to catch, what to do when you catch it, and who owns each of those answers.

## What should guardrails actually catch?

Vendors pitch a long menu: toxicity, jailbreaks, hallucination scoring, topic filters. The two categories that produce actual incidents are short to list:

- **Secrets.** API keys, tokens, connection strings, private keys, arriving inside stack traces and "can you debug this" pastes. A leaked credential is a live door into your systems, sitting in a third party's logs.
- **PII.** Emails, phone numbers, government IDs, health and financial records: the category with regulatory weight. One pasted customer export can put you in breach-notification territory.

Attachments count as much as typed text. A screenshot of a terminal carries the same credential, and a scanner that only reads the prompt field never sees it.

## Block, mask, or log

The verdict matters more than the detection, because the verdict is what your people experience. SecondGuard, the guardrails service in SecondStack, supports three, and choosing between them per category is most of the work.

**Blocking** stops the request. It is the right answer for categories severe enough that the work genuinely should not proceed. For everything else it backfires, because a person whose work just stopped will find the tool that doesn't stop it. That is how a control turns into a [shadow-AI problem](/blog/shadow-ai-detection/): the traffic is people finishing work, not people exfiltrating.

**Masking** keeps both properties. The sensitive value is replaced by a typed placeholder before the request leaves, and the original is restored in the response before anything is stored, so the model reasons about a placeholder while the person sees their own data. Placeholders stay stable across requests, so prompt caching keeps working and an agent that echoes one back into a tool call gets the real value.

**Logging** catches the category without acting on it, which is how you learn what your own traffic contains before switching anything on.

## Two designs that get proposed, and why they get switched off

Almost every rollout considers one of these first. Both are reasonable on paper, and both tend to be turned off within a quarter.

1. **Regex-only scanning** is fast and catches formatted secrets with recognizable prefixes. It falls apart on everything else: a random 40-character token matches no pattern, while commit hashes, base64 fragments and UUIDs pile up as false positives. Users learn that the scanner cries wolf, and routing around it becomes the rational move.
2. **An LLM classifier on every message** inverts the problem. Detection quality is good, latency is not. A model call on every prompt of every user taxes the whole company all day for the rare message that contains something sensitive. The chat feels slow, people notice, and the pressure to switch it off follows.

<p class="ss-pullquote">Ordinary prose and a leaked API key look different to a tokenizer long before they look different to a person.</p>

## What SecondGuard runs instead

Several detectors run side by side on the same request, each cheap where it can be: pattern packs for credentials with known shapes, a statistical check for the random-looking string no pattern claims, named-entity recognition for personal data, and an LLM classifier reserved for what patterns cannot reach, images included. Findings merge and the strictest verdict wins, and repeated content is not rescanned, so agent retries cost nothing.

The classifier sees the message unmasked, which is why it belongs on a model you host or otherwise trust with that content. A guardrails product that ships your prompts to someone else's cloud for scanning has recreated the data flow you deployed it to prevent.

One limit worth stating plainly: screening runs on the way out. What leaves your network is controlled; filtering what the model sends back is a different problem, and worth asking any vendor which of the two they mean.

## Why it belongs at the gateway

Application-level guardrails cover one application. Screening the chat app your team built leaves out every other way a prompt reaches a provider, and there are always several: an IDE plugin, a scheduled script, an agent wired up last week. Nobody can sign off on a control whose scope is "whoever remembered".

SecondGuard runs as a proxy in the request path, in front of the gateway. The ingress routes every call through it, so a request reaches a provider only after screening, whatever client sent it. If the guard proxy is down, the ingress answers with an error rather than letting traffic through.

<figure style="margin:2rem 0">
<div style="overflow-x:auto;-webkit-overflow-scrolling:touch">
<svg viewBox="0 0 940 250" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;min-width:620px;display:block" role="img" aria-label="Request path. Chat, IDEs and scripts, and agents all send requests into the ingress inside your infrastructure. The ingress routes every call through the SecondGuard proxy, which screens the outbound content; screened requests continue to the SecondGate gateway for key authentication, budgets and logging, and only then reach the provider APIs outside the perimeter. Responses come back through SecondGuard, which restores any masked values before the client stores them.">
  <defs>
    <marker id="sg-ah" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto"><path d="M0,0 L7,2.5 L0,5" fill="rgba(47,181,244,0.8)"/></marker>
    <marker id="sg-ah-back" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto"><path d="M0,0 L7,2.5 L0,5" fill="rgba(255,111,94,0.75)"/></marker>
    <linearGradient id="sg-guard-bg" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="rgba(255,111,94,0.20)"/><stop offset="100%" stop-color="rgba(255,111,94,0.08)"/></linearGradient>
    <filter id="sg-glow"><feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  </defs>
  <rect x="4" y="10" width="700" height="230" rx="8" fill="rgba(255,255,255,0.04)" stroke="rgba(255,255,255,0.14)" stroke-width="0.75" stroke-dasharray="4 3"/>
  <text x="354" y="34" text-anchor="middle" fill="rgba(255,255,255,0.76)" font-size="13.5" font-weight="600" font-family="Inter,sans-serif" letter-spacing="0.08em">YOUR INFRASTRUCTURE</text>
  <rect x="24" y="52" width="150" height="38" rx="5" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.18)" stroke-width="0.75"/>
  <text x="99" y="76" text-anchor="middle" fill="rgba(255,255,255,0.92)" font-size="14.5" font-family="Inter,sans-serif">Chat</text>
  <rect x="24" y="106" width="150" height="38" rx="5" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.18)" stroke-width="0.75"/>
  <text x="99" y="130" text-anchor="middle" fill="rgba(255,255,255,0.92)" font-size="14.5" font-family="Inter,sans-serif">IDEs &amp; scripts</text>
  <rect x="24" y="160" width="150" height="38" rx="5" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.18)" stroke-width="0.75"/>
  <text x="99" y="184" text-anchor="middle" fill="rgba(255,255,255,0.92)" font-size="14.5" font-family="Inter,sans-serif">Agents</text>
  <line x1="174" y1="71" x2="238" y2="112" stroke="rgba(47,181,244,0.5)" stroke-width="1.25" marker-end="url(#sg-ah)"/>
  <line x1="174" y1="125" x2="238" y2="125" stroke="rgba(47,181,244,0.5)" stroke-width="1.25" marker-end="url(#sg-ah)"/>
  <line x1="174" y1="179" x2="238" y2="138" stroke="rgba(47,181,244,0.5)" stroke-width="1.25" marker-end="url(#sg-ah)"/>
  <rect x="240" y="98" width="122" height="54" rx="8" fill="url(#sg-guard-bg)" stroke="rgba(255,111,94,0.7)" stroke-width="1.5" filter="url(#sg-glow)"/>
  <text x="301" y="121" text-anchor="middle" fill="rgba(255,111,94,1)" font-size="15.5" font-weight="700" font-family="Inter,sans-serif">SecondGuard</text>
  <text x="301" y="140" text-anchor="middle" fill="rgba(255,255,255,0.7)" font-size="12" font-family="Inter,sans-serif">screens egress</text>
  <line x1="362" y1="118" x2="452" y2="118" stroke="rgba(47,181,244,0.55)" stroke-width="1.25" marker-end="url(#sg-ah)"/>
  <line x1="452" y1="140" x2="364" y2="140" stroke="rgba(255,111,94,0.6)" stroke-width="1.25" stroke-dasharray="4 3" marker-end="url(#sg-ah-back)"/>
  <text x="407" y="163" text-anchor="middle" fill="rgba(255,255,255,0.55)" font-size="12" font-family="Inter,sans-serif">placeholders restored</text>
  <rect x="454" y="98" width="122" height="54" rx="8" fill="rgba(228,92,255,0.10)" stroke="rgba(228,92,255,0.6)" stroke-width="1.25"/>
  <text x="515" y="121" text-anchor="middle" fill="rgba(228,92,255,1)" font-size="15.5" font-weight="700" font-family="Inter,sans-serif">SecondGate</text>
  <text x="515" y="140" text-anchor="middle" fill="rgba(255,255,255,0.7)" font-size="12.5" font-family="Inter,sans-serif">keys · budgets · log</text>
  <line x1="576" y1="120" x2="744" y2="79" stroke="rgba(47,181,244,0.45)" stroke-width="1.25" marker-end="url(#sg-ah)"/>
  <line x1="576" y1="125" x2="744" y2="129" stroke="rgba(47,181,244,0.55)" stroke-width="1.25" marker-end="url(#sg-ah)"/>
  <line x1="576" y1="130" x2="744" y2="179" stroke="rgba(47,181,244,0.45)" stroke-width="1.25" marker-end="url(#sg-ah)"/>
  <rect x="728" y="10" width="208" height="230" rx="8" fill="rgba(255,255,255,0.03)" stroke="rgba(255,255,255,0.14)" stroke-width="0.75" stroke-dasharray="4 3"/>
  <text x="832" y="34" text-anchor="middle" fill="rgba(255,255,255,0.76)" font-size="13" font-weight="600" font-family="Inter,sans-serif" letter-spacing="0.06em">OUTSIDE</text>
  <rect x="748" y="60" width="168" height="38" rx="5" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.18)" stroke-width="0.75"/>
  <text x="832" y="84" text-anchor="middle" fill="rgba(255,255,255,0.92)" font-size="14.5" font-family="Inter,sans-serif">Anthropic API</text>
  <rect x="748" y="110" width="168" height="38" rx="5" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.18)" stroke-width="0.75"/>
  <text x="832" y="134" text-anchor="middle" fill="rgba(255,255,255,0.92)" font-size="14.5" font-family="Inter,sans-serif">OpenAI API</text>
  <rect x="748" y="160" width="168" height="38" rx="5" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.18)" stroke-width="0.75"/>
  <text x="832" y="184" text-anchor="middle" fill="rgba(255,255,255,0.92)" font-size="14.5" font-family="Inter,sans-serif">Gemini API</text>
</svg>
</div>
</figure>

The inventory question that follows is worth asking of any deployment, ours included: which paths from your network to a model do not pass through that control point? That list is the real scope of your control, and it is rarely empty.

## Where the policy attaches, and who decides

Guards bind globally or per model, which turns out to be the useful axis: the same paste can be logged on the way to a model inside your network and masked on the way to an external provider, because the risk was never the paste, it was the egress.

Two decisions come with that, and neither belongs to the platform team. **Which categories count as sensitive** is a legal and data-ownership question, and most organizations already have that list in a classification policy nobody has connected to a control. **Fail closed or fail open** is a security question: fail closed means a scanner outage stops traffic, which is right for regulated paths; fail open lets traffic flow unscanned, which is often right for low-risk internal work. It is set per guard, so the answer can differ by category. Both are defensible; having no named owner for the choice is not.

Someone also has to answer the person who got blocked at 6 p.m., and that duty needs a name before the first block lands.

## Start in log-only

Run the guards in log-only mode first, on your own traffic, for a defined window. You get your own false-positive rate instead of a vendor's benchmark, and you can replay sample text through the real detection path before anything is enforced. Publish what is screened before the first block or mask appears: a control people were told about lands differently than one they discover.

Then set the steady-state expectation, ideally with the security sponsor in the room. Most days the guards find nothing, that quiet is what you are paying for, and a quarterly report showing two findings reads like waste to anyone who was not warned.

If you are scoping guardrails for a rollout, write to [hello@secondstack.ai](mailto:hello@secondstack.ai) and we will walk you through what screening looks like in practice: which categories to start in log-only, what masking does to a working session, and where each decision lands in an organization. The hard part is rarely the detector. It is deciding which categories are worth stopping a person's work over.
