How to Scan HTML for API Keys Before Sharing

HTMLvault Team·July 26, 2026·14 min read

An AI-generated landing page can look ready for a client while carrying a live credential in a JavaScript block, an HTML comment, or a copied configuration snippet. Scanning HTML for API keys before sharing is the difference between catching an accidental exposure in the draft and discovering it after the link has been forwarded, cached, or pasted into a ticket.

AI tools write what is in their context, not what your policy allows. If the prompt, a pasted log or an old template held a test token, an internal URL or a sample customer record, that value can land in the HTML somewhere the rendered page never shows it. A polished page does not make the underlying code safe to send.

In the launch review for the AI-built pricing microsite, Dwight Brenner asked his one question: "What does a prospect see on view-source?" Nobody at Synergetics Worldwide had looked, so he read line 41 aloud: a live payment-provider secret key in the checkout script, under the comment // swap before demo. The demo had been three weeks earlier. On the project tracker, "swap key" was still green.

Where keys hide in HTML the page does not show

Review the source, not the screen. A browser renders what it is designed to render, and anyone who receives the page can open view-source, save the file or watch the network tab. Generated HTML most often carries a credential in these places:

  • Inline scripts. A config object such as const config = { apiKey: "..." }, a fetch() call with an Authorization: Bearer ... header, or an x-api-key header copied from a working prototype.
  • Comments. Debug notes and setup steps survive in comments. HTMLvault drops HTML comments (<!-- ... -->) when it stores a page, but the same file sent as an attachment keeps them, and a // or /* */ comment inside a <script> is stored exactly as written.
  • data- attributes. data-api-key, data-token and similar attributes pass values to scripts and never render.
  • Embedded JSON. A <script type="application/json"> block or serialized app state can carry tokens, internal endpoints and account IDs alongside the numbers the chart needs.
  • Hidden inputs. <input type="hidden"> fields hold session tokens, test passwords and email addresses that no visual review will see.
  • Signed asset URLs. A pre-signed storage URL in an <img src> carries a signature that grants access to whoever holds the URL until the signature expires.
  • Third-party embeds and analytics tags. An iframe with an access token in its query string, or a tag snippet copied from an internal template, can carry an account identifier or a server-side secret, and can send page context to a service nobody approved for this content.

A key that only works in staging is still an access credential, and staging systems often hold production-like data. Don't stop at the obvious prefixes either: a page with no classic API key can still expose a session token, an internal hostname or a customer's phone number.

Browser preview of a pricing page beside the seven places a secret scan reads Where an API key hides from the preview BROWSER PREVIEW WHAT THE SCAN READS Request demo Renders clean Inline scripts API_KEY HTML and JS comments data- attributes Embedded JSON Hidden inputs Signed asset URLs Third-party embeds and tags
A pricing page can pass every visual check and still hand a working key to anyone who opens view-source, so scan HTML for API keys in all seven places instead of trusting the preview.

Scan the source before a link exists

The cheapest moment to catch a secret is while the HTML is still a draft in the conversation or the script that produced it. Once a URL exists it can be forwarded, and deleting the page later recalls none of the copies.

HTMLvault checks at two points. Every publish and every edit runs the account's PII and secret-scanning policy, which is regex-based (no AI model, zero tokens), and checks every outside URL in the page against Google's unsafe-URL lists. A refused publish returns the findings instead of a URL. Before that, an AI tool connected through the HTMLvault MCP server can call scan_html on its own draft and fix what comes back before it calls create_link. scan_html exists only as an MCP tool. The REST API has no scan endpoint, so a script publishing over REST relies on the check at publish: on a refusal, it fixes the HTML and tries again.

The scan flags nine categories: api_key, ssn, financial, passport, address, person, dob, email and phone. For keys, read the api_key category. The other eight are there because the paste that carries a credential usually carries personal data too. Treat the secret-scanning findings as a stop sign until someone decides whether each one is a real credential, a placeholder or a false positive.

Say you ask Claude to build a customer-facing status page from the log excerpt of last night's incident, and its draft includes this:

<script>
  const statusConfig = {
    endpoint: "https://status-internal.example.com/v2",
    apiKey: "ak_live_9f2c41d7e8b04a6c93d1e5"
  };
</script>
<input type="hidden" name="oncall" value="oncall-lead@example.com">
  1. Claude calls scan_html on the draft before publishing. Put that instruction in the Claude project instructions so it happens every time.
  2. The scan returns two findings: api_key in the config object Claude carried over from the log, and email in the hidden input.
  3. Claude deletes the config block, because a status page needs no live calls, and deletes the hidden input, because no customer needs the on-call address. A second scan_html comes back clean.
  4. Claude calls create_link with a 24-hour expiry. The publish runs the same policy check and returns the link.
  5. If the page needs a password, add it afterwards on the web, from the links dashboard. MCP create_link and the REST API cannot set one.
Because scan_html runs while the page is still a draft in the conversation, the key copied from the log excerpt never reaches a URL that someone could forward.

The unsafe version of that page never got a URL. The key did travel once, inside the log you pasted into Claude, so tell whoever owns it and let them decide on rotation. MCP create_link accepts expiries of 1h, 24h, 3d, 7d, 30d or never. On the web, Pro and above choose from the same windows plus 1 year, and an expiry can be changed or extended after sending. Free links expire after 30 days. For which window fits which send, see link expiry settings.

Pattern, context, policy: keeping alerts worth reading

A scanner people stop reading is worse than none, because it trains them to click past the one finding that was real. Findings stay trusted when detection runs in three layers.

Pattern. The first layer matches shapes: known key prefixes, bearer headers, JWT structure, long random strings. HTMLvault's scanner works at this layer. Its regular expressions run on every publish and edit, on every plan, with no model call and no token cost. Patterns are fast and predictable, and they flag anything with the right shape, including an ID that only looks like a key.

Context. The second layer asks where the match sits. A 32-character string inside an Authorization header, assigned to a variable named apiKey or passed to fetch() is almost certainly a credential. The same 32 characters as a CSS class, a commit hash in the footer or a row ID in a table almost certainly are not. Whoever reads the findings, a reviewer or the agent itself, supplies that context. On Teams and Enterprise, a BYOK AI scan adds a model's reading of it through your own Anthropic, OpenAI or Google API key, so HTMLvault never pays for the tokens. That scan runs on a link after it is published, as a second look rather than the gate. Regex and AI miss different things; see where regex and AI scanning each fail.

Policy. The third layer decides what a finding does. In HTMLvault, the account's PII and secret-scanning policy (on Teams and Enterprise, its Publishing Rules) decides which findings refuse a publish, and a refused publish returns those findings instead of a URL. Your team still needs a written answer for the rest: what never goes out (live keys, private keys, connection strings), what a reviewer can clear, and who that reviewer is.

Publishable identifiers vs secrets

Not every string that resembles a credential should be removed. Some services issue identifiers meant to reach the browser: analytics measurement IDs, public map keys and the publishable half of a payment provider's key pair.

The question is not whether a value has the word "key" in its name. Ask what the value can do if a stranger copies it. If it can read private data, write data, send messages, charge a card, administer an account, spend a quota or impersonate a service, it does not belong in distributed HTML. Paired keys make the test concrete: the publishable payment key a checkout page loads is fine, and the secret key that issues refunds is not. An analytics measurement ID belongs in the tag; the API secret for sending server-side events does not.

The same test covers your own tooling. The HTMLvault API key an automation uses to publish stays with the automation, never in the page it publishes.

When a value is intentionally public, record that decision. The next reviewer can then tell an approved client-side identifier from a credential that survived because everyone assumed someone else had checked it.

Triage a finding: block, review or proceed; rotate first, delete second

Every finding lands on one of three rungs. Clean: nothing flagged, so publish with an expiry that fits the send. Low confidence: a string has the shape of a key but no context that says it is one, so the person who owns the key decides. High confidence: a known key format, or a value inside an Authorization header or an apiKey assignment, so the page does not go out until the value is removed or that owner confirms in writing that it is a publishable identifier.

Decision ladder for a scan finding: clean, low confidence, high confidence Three outcomes for a scan finding FINDING RESULT OUTCOME Nothing flagged no category matched CLEAN PROCEED publish with an expiry Token-shaped string no label, no auth header nearby LOW CONFIDENCE REVIEW the key's owner decides Key-format string inside an Authorization header HIGH CONFIDENCE BLOCK until removed or approved
The same 32-character string is a review item when it sits in a CSS class and a hard block when it sits inside an Authorization header, which is why triage reads context before it reads the value.

To check whether a flagged value is live, look it up in the issuing service's credential list, and check which environment issued it and what it is allowed to do. Do not paste the suspected value into chat, email or a public issue to ask whether it is real; that turns an internal finding into a wider distribution.

If the value is live and has already left the draft (in a sent link, an attachment, a pasted log), rotate or revoke it first. Removing it from the HTML is necessary, but it invalidates none of the copies already saved, forwarded or cached. Rotation replaces the credential and revocation disables it; which one fits depends on whether an application still needs the key.

Then fix the page. On a published HTMLvault link, patch_link can replace the key's exact string with a placeholder (add replace_all if it appears more than once) without changing the URL, and the edit runs the same policy check as a publish. delete_link removes the page entirely. For a sales proposal, client report or campaign page, remove the configuration example altogether; keep a neutral placeholder such as YOUR_API_KEY only when the recipient really needs implementation guidance.

A flagged string can be a false positive: long random IDs, example hashes and opaque references often resemble tokens. Don't teach people to dismiss findings by default. Record why a finding was cleared in your normal change record. On Teams and Enterprise, the PII audit log keeps one scan event per publish or edit, but it does not record who reviewed a finding or why, so the rationale lives in your own record.

Keep privileged calls off the client

A scan catches an accidental exposure; architecture keeps the same mistake from coming back. Browser-delivered HTML is client-side material, and anyone who receives it can inspect, copy and modify it. Treat a secret embedded there as disclosed.

An HTMLvault page is served as written, so its scripts run in every recipient's browser. That is what lets a Chart.js dashboard draw itself, and it is also what puts a key inside a fetch() call in front of every recipient.

For a report, proposal or status page, bake the data in. Have the AI tool pull the numbers where it runs, with its own credentials, and write the results into the HTML as tables, inline SVG or a JSON block of values, so the page makes no live calls at all.

When a page needs live data, move the privileged call to a server-side service. The browser calls your controlled endpoint, and that endpoint uses the credential without returning it to the visitor. Apply authentication, authorization, rate limits and the narrowest credential scope the workflow needs. A proxy adds implementation work and some latency, and for a static demonstration mocked data is often simpler. For a production-facing page, a small backend costs less than a reusable credential circulating in page source.

Make it the default in automation, and scan again on every edit

A check people have to remember gets skipped at quarter-end, so write it into the automation. The rule for an agent: call scan_html on the final HTML, fix every finding, scan again, then call create_link, or create_links for a batch. If a publish is refused, read the findings it returns and fix the HTML; never resend it unchanged.

Three publishing paths change what the scan sees or when the check runs:

  • Batches. create_links (or POST /api/v1/links/batch) publishes up to 100 links from one template, with {{name}} placeholders and a vars map per row. Values in the vars only exist after rendering, so a scan of the bare template misses whatever the rows bring in. Scan a rendered row as well, and treat the vars source (a CRM export, a Clay table) as part of the page.
  • Append mode. A large page built with create_draft_link and append_html runs no checks until it is finalized with finalize: true, and update_link is refused on a draft. The check happens at finalize, and a refused finalize returns findings like any other publish.
  • Edits and regenerations. update_link and patch_link run the same policy check as a first publish. A template that was clean yesterday can carry a key today after a prompt change, a new data source or a swapped embed, so every regeneration gets a fresh scan instead of yesterday's result.

Three failure modes undo all of this:

  • Detection without enforcement. A scanner exists somewhere in the stack, but the path people actually publish through never calls it.
  • A manual-review bottleneck. Every page waits on one reviewer, so people route around the queue with whatever tool is faster.
  • Ignoring adjacent PII. A page with no API key can still carry an email column, a phone number or a date of birth, which is why PII detection runs in the same pass.

What a scan cannot catch

  • Shapes it has never seen. Regex finds formats, not intent. A key in a bespoke internal format, split across concatenated strings, base64-encoded or assembled at runtime can pass, and a convincing placeholder can be flagged. Treat the scan as a floor, not a guarantee.
  • Files the page loads. The scan reads the document you publish. A script or stylesheet loaded from another host is checked against Google's unsafe-URL lists, not read for keys.
  • Meaning in prose. A paragraph that describes an internal system, or a client named in a sentence, has no format to match. The BYOK AI scan on Teams and Enterprise reads for that kind of context, after publish.
  • A key that already shipped. A password (set on the web) and an expiry limit who can open the page and for how long, but every recipient who can open it can copy a key from the source. Links are never indexed, which closes the search path, not the forwarding path. After a link expires, its content stays stored until the account's single retention setting runs out (7 days to 2 years on paid plans, 7 to 90 days on Free, counted from expiry); then the content and its per-visitor view data are deleted.

Run scan, fix, publish in that order and a finding reaches you while the HTML is still a draft in the conversation, where the fix is deleting one line. Found after the link is out, the same finding means rotating a key and writing a note to every recipient.

api-key-scanningsecret-detectionhtml-securitycredential-exposurepii-detectionpublishing-workflowmcpai-generated-html
HTMLvault

Publish your AI creations. See who reads them.

Proposals, dashboards and reports from Claude, ChatGPT or any AI tool become private links on your own domain. Update them after you send, and see who read what, down to the scroll.

Create a link

Related posts