Do AI agents leak secrets? Yes. The publish step is where it becomes public.
An agent assembles a page from whatever is in its context, and context routinely carries live credentials: the token that fetched the data, the key in a pasted config. Publishing is the moment a private mistake becomes a public one. Here is how that happens, what a pre-flight scan catches, and where the guardrail honestly ends.
Do AI agents leak secrets when they publish?
Yes, and not because the models are careless. An agent builds output from whatever is in its context window, and context routinely contains live credentials: the bearer token it used to fetch the data, the API key in an environment dump, the config file pasted three turns ago. When the agent publishes, anything that slid into the output goes live with it. This is a context problem, not a model-quality problem; a better model writes better HTML around the same leaked key. The fix belongs at the publish boundary: scan the file before it goes live and refuse to publish on findings unless someone overrides on purpose. Stacktree runs that scan on every publish, and its MCP publish tool defaults to block.
A context problem, not a model problem
Secrets reach an agent's context through completely normal work. Three routes cover most incidents:
- The agent authenticated to get the data. The
Authorizationheader sits in the transcript right next to the response it fetched. To the model, both are just material. - A human pasted something. An env dump to explain a bug, a config file to ask a question, a curl command that worked. All of it stays in context for the rest of the session.
- A tool read it. File tools read
.env; shell tools print tokens in command output; API responses sometimes echo the caller's own key back in an account object.
From there the leak is not a malfunction. Asked to "build a dashboard from this response," the model completes a document using everything relevant at hand, and the token is in the relevant pile. OWASP names this risk class directly: sensitive information disclosure is LLM02 in its Top 10 for LLM applications, covering credentials and PII surfacing in model output. And it shows up at scale in the wild: GitGuardian's 2026 Secrets Sprawl report counted 28.65 million new hardcoded secrets on public GitHub in 2025 and measured Claude Code-assisted commits leaking at 3.2%, about twice the 1.5% baseline for all public commits.
A publish step is where the severity jumps. A key sitting in a conversation is contained; a key in a commit is bad; a key on a published URL has left your control entirely. That is why the guardrail belongs at publish time, after the last edit and before the first byte is served.
How it happens: a dashboard with a bearer token in it
A concrete, unexotic version. You ask an agent for a usage dashboard for your SaaS account. It calls your billing API, gets JSON back, and writes a tidy self-contained HTML page. Somewhere in that page, one of three common shapes appears:
// generated dashboard, abbreviated
const data = await fetch('https://api.example.com/v1/usage', {
headers: { Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...' }
}); - The fetch call is kept in a
<script>tag, header included, "so the dashboard refreshes live." - The raw API response is inlined as a data blob, and the account object inside it contains an
api_keyfield nobody asked for. - The working curl command is preserved in a code comment as documentation.
Then the natural next instruction: "publish it so I can send it to my cofounder." On a host with no scan, that token is now on a URL. On Stacktree over MCP, the publish fails before anything is stored, with a structured error the agent can read and act on:
HTTP 422
{
"error": "pii_detected",
"hits": [
{ "category": "jwt", "sample": "eyJ***Zw", "count": 1 }
]
} The agent removes the token, republishes clean HTML, and the conversation moves on. The interesting part is what did not happen: no human had to notice the token scrolling past, and the error did not echo the secret back into the transcript, because samples are redacted to a few leading characters and the last two.
What the pre-flight scan catches
The scan runs on every publish before it completes, over every text file in the upload (html, htm, md, txt, css, js, mjs, json, svg, xml), including each matching file inside a zip. These are the pattern classes, exactly as implemented:
| Category | What it matches | How it is checked |
|---|---|---|
| API keys and tokens | sk- keys (OpenAI, Anthropic), Stripe sk_live_ / sk_test_, Slack xoxb- and the rest of the xox family, GitHub ghp_ / gho_ / github_pat_, AWS AKIA access key IDs, Google AIza keys | Known prefix plus a minimum length |
| JWTs | eyJ followed by two more base64url segments, dot-separated | Structural match; catches most OAuth bearer tokens |
| Credit card numbers | 13 to 19 digit runs, spaces or hyphens allowed | Luhn checksum, so arbitrary digit runs do not false-positive |
| US Social Security numbers | The ddd-dd-dddd shape | Pattern match |
| Email addresses | Anything user@domain shaped | Pattern match; often intentional, which is what warn mode is for |
Findings come back redacted: a truncated sample per category (first characters, then the last two; emails as the first letter plus the domain) and a count. The full match is never echoed, so the scan's own output cannot re-leak the secret into logs or a conversation transcript.
Warn vs block, and why the agent path blocks
The scan has three modes, set per publish with pii_check: off, warn, and block. Which default you get depends on who is publishing:
- The raw HTTP API defaults to warn. A human is presumed in the loop. The publish succeeds and the response carries an
X-Stacktree-Pii-Warningheader listing the categories found, so the caller can look at the page and decide. This is the right default for humans partly because one category, email addresses, is frequently on a page on purpose. - The MCP publish tool defaults to block. Agents operate autonomously; there is no keyboard to double-check a warning that scrolls past. When findings turn up, the publish is rejected with the
pii_detectederror above, and nothing has been written anywhere. The agent can fix the content and retry, or, if the findings are genuinely intentional, passpii_check: "warn"explicitly. The override is an argument in the tool call, visible in the transcript, not a silent setting.
Block-by-default sits alongside the rest of Stacktree's MCP posture: publishes land on unguessable token URLs, expire after 7 days unless the agent asks for "never", and serve under a strict CSP. The shared principle is that an autonomous path gets tighter defaults than a supervised one, and every loosening is an explicit, logged choice.
What a regex net cannot promise
This scan is a net at the last exit, and it is worth being precise about the mesh size:
- Shapeless secrets pass through. A random hex token with no vendor prefix, a database connection string with an embedded password, an internal token your company minted, a human password like
hunter2: none of these have a shape a pattern can recognize, and there is no entropy analysis to catch them statistically. - It is not a substitute for repo scanning. Dedicated scanners (GitGuardian, TruffleHog, gitleaks) run hundreds of detectors and verify candidates against live providers. If a key is sitting in your codebase, fix that first; a publish-time gate is the backstop, not the system.
- Very large text files are skipped. Text files over 2 MB are not scanned, a deliberate cost bound on a check that has to run inline on every publish.
- End-to-end-encrypted publishes are not scanned at all. With E2E enabled, the server receives ciphertext and the decryption key stays in the URL fragment, which never reaches the server. A host that cannot read your content cannot scan it either. That is the tradeoff stated plainly: E2E gives you a host that is blind to your data, and with it the full responsibility for what the plaintext contains.
None of this makes the gate pointless. The failure mode it targets, a live vendor credential inlined by an agent mid-flow, is precisely the case where the patterns are strongest: vendor keys have prefixes and bearer tokens are JWTs by construction. It catches the common disaster reliably and the uncommon one never, and you should plan around exactly that.
If a secret already shipped
Treat the credential as compromised from the moment the page went live, whoever opened it. The uncomfortable industry data: GitGuardian found roughly 70% of secrets confirmed valid in 2022 were still valid in early 2025, and over 64% at the start of 2026. Most leaked keys are never rotated, which is the entire attacker business model. So, in order:
- Rotate or revoke the credential at the provider, first. AWS's own incident guidance is a good template: disable the exposed key immediately (disabled keys can be re-enabled if something unexpected depended on them), invalidate any temporary credentials minted from it, then audit what it was used for.
- Then clean up the page.
update_sitereplaces the content in place at the same URL;delete_siteremoves it;set_expiryretires it on a schedule. Any of these ends the ongoing exposure, but note the order: removing the page limits future readers and does nothing about anyone who already opened it. Rotation is the fix; deletion is hygiene. - If you meant to hand over a secret, use a one-shot channel next time. A credential that must reach exactly one person does not belong on a durable page. Publish it with
burn_after_read, which deletes the page after the first view, or use a one-time view link.
The pattern to internalize is the same one the publish gate encodes: the cost of a secret leak is set by how far it traveled and how long it stayed valid, and both of those are decided in the minutes around the publish step. Guard that step by default, override it consciously, and rotate without debate when the net misses.
Frequent questions
Can an AI agent leak API keys? +
How do I prevent secrets in published HTML? +
What happens if my agent publishes a password? +
Does Stacktree scan for PII? +
Can the agent override the block? +
Related guides
- Share an HTML file with a client The handoff flow this scan protects: private link, gates, revocation.
- MCP Apps made HTML the official UI of agents Agents emitting HTML is now standardized. What happens when it leaves the session.
- One-time view links For secrets you mean to share: a link that works exactly once.
- The MCP publish tool publish_html and its defaults (block-mode scan, 7-day expiry) in detail.
Sources and further reading
- GitGuardian: The State of Secrets Sprawl 2026 ↗ 28.65M new secrets on public GitHub in 2025; Claude Code-assisted commits leak at 3.2% vs a 1.5% all-commits baseline; most leaked credentials stay valid for years.
- OWASP GenAI: LLM02:2025 Sensitive Information Disclosure ↗ Credentials and PII in model output as a named, ranked risk class, with mitigation guidance.
- AWS Security Blog: What to do if you inadvertently expose an AWS access key ↗ The provider's own remediation order: disable the credential first, then audit usage.
Publish behind a guardrail.
Every Stacktree publish is scanned before it goes live, and the MCP default on findings is to block. Private, unguessable URLs for everything your agent ships.
Sign up free →