Back
Vulnerabilities2 Sept 2026 · 4 min read

29 million secrets on GitHub: what AI changes about leaked keys

by detektd team

Brass dial of an old safe on a weathered wooden door
Photo: Leeroy · CC0 · StockSnap

Every year, GitGuardian scans every public commit on GitHub to count the secrets (API keys, tokens, passwords, connection strings) written in plain text. The 2026 edition of its State of Secrets Sprawl report, published in March, covers 2025: 1.94 billion public commits (+43%), and 28.65 million new exposed secrets, up 34% year over year. That's the largest increase the company has ever measured.

0

secrets hardcoded in public GitHub commits in 2025, up 34% year over year (GitGuardian, March 2026)

AI, the top growth driver

The most striking figure concerns AI services. GitGuardian detected 1,275,105 secrets tied to AI providers in 2025, up 81%. Eight of the ten fastest-growing secret types are AI-related; DeepSeek API keys alone account for more than 113,000 exposures. And the infrastructure around models (gateways, vector databases, orchestration tools) leaks five times faster than the keys of the main model providers themselves.

The report also looked at code written with assistants. Among commits identifiable as Claude Code-assisted, the share containing a secret reaches 3.2%, versus 1.5% on average across public GitHub. GitGuardian is careful to note that the developer accepts or rejects the suggestion. But the mechanism is easy to reconstruct. When you ask an agent to "make an integration work", the most direct path is to put the key where the code needs it.

A newcomer in the numbers: MCP configuration files, which describe the tools wired into an assistant (.cursor/mcp.json, .vscode/mcp.json, claude_desktop_config.json...). GitGuardian found 24,008 unique secrets in them on public GitHub, 2,117 still valid. The cause, according to the report: documentation that often shows the key right in the file, which then gets committed along with the rest of the project.

How a key ends up in the browser

A secret committed to a public repo is serious. A secret embedded in the JavaScript served to visitors is just as serious, and that second path is specific to modern web development. Bundlers replace certain environment variables with their value at build time, and the result is a file any visitor downloads.

The rule is the same everywhere, only the prefix changes. In Next.js, any variable prefixed NEXT_PUBLIC_ is inlined into the client bundle. In Vite, it's VITE_, via import.meta.env. In Create React App, REACT_APP_. Those prefixes exist precisely to mark what may be public. The trap is prefixing a server key "because otherwise it doesn't work client-side", which is exactly the fix a hurried agent suggests when a call fails in the browser.

What ends up in the client bundle (Next.js)
# .env.local
NEXT_PUBLIC_SUPABASE_URL=...        # inlined: public by design, fine
NEXT_PUBLIC_OPENAI_API_KEY=sk-...   # inlined: every visitor can read it
OPENAI_API_KEY=sk-...               # server-only: never reaches the browser

Next.js provides a guardrail for the opposite mistake: importing a server module into a client component. Adding import "server-only" at the top of a file that handles secrets makes any attempt to import it client-side fail the build, instead of silently bundling its contents.

lib/openai.ts: forbidden client-side
import "server-only";

export async function complete(prompt: string) {
  const res = await fetch("https://api.openai.com/v1/responses", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
    body: JSON.stringify({ model: "gpt-5", input: prompt }),
  });
  return res.json();
}

How detectors find a key

Modern secret detection relies on three layers. First, prefixes: most providers give their keys a recognizable shape, precisely to make them detectable: AKIA for an AWS access key ID, ghp_ for a GitHub personal token, sk_live_ for a Stripe secret key, xoxb- for a Slack bot token. Then entropy: a 40-character random string doesn't look like a word. Finally, validation: a serious detector checks, where the provider allows it, whether the key is still live, which separates a historical leak from an ongoing incident.

The report points out a limit of that last layer: prioritizing only validated secrets misses 46% of critical ones. Many keys simply can't be tested from the outside (internal database credentials, keys for self-hosted services), and they're often the most sensitive.

The real problem: nobody revokes

The report's most worrying figure isn't about leaks but about fixes: 64% of valid secrets detected in 2022 were still valid in January 2026. Deleting the file, or even rewriting Git history, achieves nothing if the key stays active. A public commit is cloned, cached and indexed within minutes; the only real fix is revocation.

0%

of valid secrets detected in 2022 were still active in January 2026 (GitGuardian)

Public repos aren't even the worst place. Internal repositories are six times more likely to contain secrets, and 28% of incidents start outside code: Slack, Jira, Confluence, where leaks are 13 points more likely to be critical. The report also analyzes machines compromised by the second wave of the Shai-Hulud worm: 6,943 machines, 33,185 unique secrets, and 59% of them were CI/CD runners, not developer laptops.

A four-link defense chain

  1. Before the commit: a pre-commit hook (gitleaks, ggshield) and GitHub push protection, which blocks a push containing a recognized secret.
  2. In the code: no server key behind a public prefix, import "server-only" on sensitive modules, and environment variable references in MCP configs rather than values.
  3. In production: check what the site actually serves (bundles, exposed files), since that's what an attacker sees, whatever the state of the repo.
  4. After a leak: revoke first, clean up second. And replace long-lived keys with short-lived credentials wherever possible (OIDC in CI, temporary tokens).

The third link is the one detektd covers. On every scan, the JavaScript bundles the app actually serves are analyzed for recognizable keys (AI providers, Stripe, AWS, third-party services), and files that should never be public (.env, backups, configs) are checked at the site root.