By · Founder, Stacktree · Last updated
blog · safety

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.

Get started free

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 Authorization header 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_key field 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-Warning header 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_detected error above, and nothing has been written anywhere. The agent can fix the content and retry, or, if the findings are genuinely intentional, pass pii_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:

  1. 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.
  2. Then clean up the page. update_site replaces the content in place at the same URL; delete_site removes it; set_expiry retires 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.
  3. 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.

FAQ

Frequent questions

Can an AI agent leak API keys? +
Yes, and it is one of the most ordinary agent failure modes, not an exotic attack. Keys enter the context window through normal work: the agent authenticates to fetch data, reads a config file, or the user pastes an environment dump to explain a problem. The model then treats everything in context as material for the output. GitGuardian measured Claude Code-assisted commits leaking secrets at a 3.2% rate, roughly double the 1.5% baseline across all public GitHub commits. A publish tool with no scan turns that mistake into a live URL.
How do I prevent secrets in published HTML? +
In layers. Keep credentials out of agent context where you can: scoped tokens, secret managers, not a .env file sitting in the working directory. Run a dedicated secret scanner over your repo and CI. Then gate the publish step itself. Stacktree scans every non-encrypted publish for API key prefixes, JWTs, card numbers, SSNs, and email addresses before the publish completes, and when the call comes from an agent over MCP the default on findings is to block, not warn.
What happens if my agent publishes a password? +
It depends on the shape of the credential. A JWT or an API key with a recognizable prefix gets caught by the pre-flight scan; on the MCP path the publish fails with a structured pii_detected error before anything is stored. A freeform human password has no shape a pattern can recognize, so assume it shipped: rotate the password first, then clean up with update_site or delete_site. Deleting the page does not undo the exposure. Rotating the credential does.
Does Stacktree scan for PII? +
Yes. Every publish except end-to-end-encrypted ones is scanned before it completes: API keys and tokens (the sk-, sk_live_/sk_test_, xoxb-, ghp_/gho_, github_pat_, AKIA, and AIza families), JWTs, email addresses, credit card numbers validated with the Luhn checksum, and US Social Security numbers. Three modes per publish: off, warn, block. The raw API defaults to warn and flags findings in an X-Stacktree-Pii-Warning response header; the MCP publish tool defaults to block and returns redacted findings. Samples in findings are truncated, so the error itself never repeats the full secret.
Can the agent override the block? +
Yes, deliberately. The MCP tool schema documents pii_check: "warn" as the override, so an agent that decides the findings are intentional (a contact email on a marketing page, say) has to pass the override as an explicit argument in the tool call. The decision is visible in the conversation transcript instead of buried in a default.
Keep reading

Related guides

References

Sources and further reading

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 →