Hush Security analyzed ~82,000 Model Context Protocol (MCP) configuration files on public GitHub, the files that AI coding agents (Claude Code, Cursor, VS Code, Windsurf, Gemini, opencode, OpenAI Codex, JetBrains, and more) read to connect to their tools. Unlike .env, these files are designed to be committed to version control. We found that 12% of credential slots hardcode a credential literal, that 55% of those secrets have no recognizable token shape (the signal value-pattern scanning relies on to find secrets), and that 24% of them are, by the issuer's own design, both broad-scope and non-expiring. This is a non-human identity (NHI) governance problem, not a secret-hygiene one.
~82,000 public MCP configuration files across more than a dozen coding agents, a fast-growing, standardizing surface.
44% of credential slots use safe patterns (${VAR} expansion, prompts, secret managers); 12% hardcode a credential literal into the committed file.
55% of hardcoded secrets have no vendor-recognizable token shape, the primary signal value-pattern scanners (gitleaks, trufflehog, GitHub secret scanning) rely on to detect a secret.
31% are opaque bearer tokens for internal MCP servers that no provider-pattern scanner can identify at all.
Of the credential-bearing configs we traced, 243 had a secret deleted from the file yet still readable in Git history and 1,394 still carry one at HEAD.
What is an MCP Configuration File, and Why Does it Hold Secrets?
A Model Context Protocol (MCP) configuration file tells an AI agent which tool servers to launch and how to authenticate to them. When you add a GitHub, Postgres, or Slack integration to Claude Code, Cursor, or VS Code, the client writes that setup (the command, its arguments, and an env block or auth header of credentials) into a JSON file in the repository: .mcp.json, .cursor/mcp.json, or .vscode/mcp.json. (Claude Desktop's claude_desktop_config.json uses the same format but lives in the user's application-support directory, so it reaches a repository only when someone commits it.)
And that is the whole problem. Every security team has internalized one rule about secrets in source control: .env is gitignored. MCP configuration files break that reflex on purpose: they exist so a team can share agent tooling through the repository. Check in the config, and every teammate's editor picks up the same MCP servers. They are designed to be committed. And they describe exactly how an autonomous agent authenticates to GitHub, to a database, to Slack, to an internal service.
A file meant for version control that names the credentials a machine identity uses: that makes MCP config the canonical home for long-lived, over-privileged non-human identities that live in Git by design. This is not a secret-hygiene story about a forgotten .gitignore. It is a governance story about a new class of machine identity with no owner, no expiry, and no lifecycle. Here is what the state of MCP configuration on public GitHub actually looks like.
How we Measured Credential Exposure in MCP Configs
We enumerated the MCP config filenames used across the major coding agents on public GitHub via the code-search API, then parsed each file and classified every credential slot (an env value or auth header) as a hardcoded literal, a ${VAR} reference, a client-native prompt, a secret-manager reference, a placeholder, or empty. Hardcoded literals were further identified by provider pattern and Shannon entropy.
Treat the population counts as order-of-magnitude estimates, not a precise census: GitHub code search indexes only default branches, excludes forks, and caps results per query, so every number here is a lower bound on real exposure. A few of these files (opencode's opencode.json, Gemini's and Zed's settings.json, Codex's config.toml) are general client configs that carry an optional MCP section, so for those we count only the sampled share that actually declares an MCP server, not every file of that name.
We ran this as defensively as we ask our customers to. Credentials were classified by shape and entropy alone and never validated against a live provider API (a validation call authenticates as the leaked identity, which is exactly the harm the research is about). So we report credential shape, not credential validity: a "hardcoded credential literal" is a value whose shape and entropy mark it as a real secret rather than a placeholder, not one we confirmed still authenticates. Some have since been rotated, and every figure below is exposure, not confirmed live access. Because the tokens with no recognizable shape are the hardest to take on faith, we profiled every one of them by key name and value shape alone (length, entropy, character class, never the value): they are overwhelmingly real vendor API keys, bearer tokens, and database passwords, with non-secret identifiers a negligible share. We stored no raw secret values, registered no domains, and published no packages. All findings are aggregated and anonymized, with no repository or author identifiable, and responsible disclosure precedes any publication.
FINDING 1How Widespread Committed MCP Configuration is
MCP config is not an edge case; it is a rapidly standardizing surface across every major coding agent. Each of these files is a place a credential can live in version control without violating any .gitignore convention: for the project-level configs, committing them is the intended workflow.
FINDING 2How Often the Secret is Actually Committed
This is the number that matters, and to get it right we count only credential slots (an env/header value whose key names a secret, or that holds one), excluding generic config like PATH or AWS_REGION that would otherwise pad the "safe" column. Of those, 44% already use the safe pattern, ${VAR} expansion, client-native prompting, or a secret-manager reference, leaving the value out of the repo. Another 38% are template placeholders (your-api-key-here) that never held a real secret. And 12% hardcode a credential literal into the committed file.
The good news is real: when a value is actually supplied, ${VAR} expansion is the most common way to do it, so the ecosystem's defaults are pulling the right way. The bad news is that a double-digit share of an enormous, fast-growing population is a large absolute number of machine credentials sitting in public git. And as the next finding shows, they are not the low-value kind.
FINDING 3What's Actually in the Hardcoded Slots
Two things stand out. First, the provider spread is exactly the high-value set you would fear: GitHub PATs (including the GITHUB_PERSONAL_ACCESS_TOKEN slot), Anthropic and other model-provider keys, Slack and Notion workspace tokens, Postgres and Mongo connection strings with embedded passwords, and cloud keys.
Second, and this is what makes it an NHI problem rather than a hygiene one: 55% of these hardcoded secrets have no vendor-recognizable token shape. Value-pattern scanners (gitleaks, trufflehog, GitHub secret scanning) find secrets mainly by matching that shape, a prefix like ghp_ or a connection-string URI; a few of these vendors also register their keys for platform secret-scanning, but that reaches only a minority of what is here. Fewer than half of what we found carries a recognizable shape at all; the rest are high-entropy tokens that match no known pattern, including opaque bearer tokens for internal MCP servers that no provider signature describes. Finding those needs detection that understands MCP config structure: a high-entropy literal in a credential-named slot, treated as an identity rather than a string.
FINDING 4The Scope and Lifetime of a Leaked Credential
Counting leaked secrets undersells the problem; the question is what each one can do. We classified every hardcoded literal by the by-design capability of its credential type: the scope and expiry the issuer defines, never anything learned by probing the live token.
Among leaked literals whose type has a defined scope, 53% are organization-, account-, workspace-, or database-wide. Among those with a defined expiry policy, 80% do not expire by default. Both rates are conditioned on a classifiable type, and they exclude the 55% of literals that are opaque tokens whose scope and expiry cannot be inferred from outside. Across all hardcoded literals, 24% are both broad-scope and non-expiring: not a narrow, short-lived, scoped token, but a durable key that grants wide access and never lapses on its own.
Worse, it grants that access to an agent, not a person. An MCP server acts under this identity autonomously: no human in the loop, no joiner-mover-leaver process to revoke it, often no record of who created it. It is a non-human identity in the most literal sense, and an unmanaged one.
FINDING 5Unmanaged NHIs Live Forever
We traced the commit history of every one of the 7,681 credential-bearing configs in the corpus, reading each file back through up to seven revisions before HEAD. 243 of them had a hardcoded secret that was later deleted from the current file, yet the secret is still readable in an earlier commit. Deleting the line in a new commit does not remove the secret: the old blob stays permanently in history, and GitHub still serves it. (A further 1,394 never even deleted it and still carry a hardcoded literal at HEAD.) The only thing that actually ends the exposure is rotating the credential at the provider.
We say nothing here about how many teams do purge a secret by rewriting history, because those cases are unobservable by definition: a rewritten blob is gone, so we cannot count it. What we can show is the opposite and more common case, verified directly in the data: a developer "removes" a secret, and it is still there.
This is the lifecycle problem in one sentence: an unmanaged NHI, once committed, persists until someone rotates the credential at the provider, which requires knowing it leaked, knowing who owns it, and having the authority to revoke it. For an ownerless agent identity, all three are usually missing. Deleting the line feels like remediation and accomplishes nothing.
Why this is a Non-Human-Identity Problem, Not a Secret-Hygiene One
Every instinct trained on .env says "scan for secrets, block the commit, rotate what leaked." That instinct is necessary here and completely insufficient, for three reasons this data makes concrete:
The file is committed on purpose. You cannot gitignore your way out; the workflow depends on the config being in the repo. The control moves from "keep the file out of git" to "keep the secret out of the file."
The highest-risk credentials have no shape to match. The custom bearer tokens for internal MCP servers (Finding 3) carry no vendor prefix, so value-pattern matching has nothing to key on. Detecting them requires understanding MCP config shape.
These are identities, not strings. Scope, expiry, and ownership (Findings 4 and 5) are identity-governance properties. An agent authenticating with an org-wide, non-expiring, ownerless key is an unmanaged NHI whether or not any regex ever flags the string.
This is the case Hush makes about non-human identities generally: you cannot secure what you cannot see or govern, and machine identities have outgrown the tooling built for human ones and isolated secrets. MCP config is where that gap becomes vivid: a file designed to carry a machine identity into version control.
Hardening Your MCP Configuration
For anyone shipping MCP config today:
Never commit an inline secret. Use ${VAR} / ${env:VAR} expansion so the value resolves at launch and never enters the repo; on VS Code use ${input:…} so the client prompts; for shared setups reference a secret manager (op://, vault:).
Detect the secrets that prefix-based scanning misses. More than half of what leaks here has no recognizable token shape, so matching on vendor prefixes is not enough. You need detection that understands MCP config structure and treats each credential as an identity, flagging any high-entropy literal in a credential-named slot before it ever merges, not only the tokens that happen to start with ghp_.
If a secret was ever committed, rotate it at the provider. Removing the line does not remove it from history; rotation is the only real fix.
Give every agent identity an owner and an expiry. Scope the token to the minimum the server needs, set an expiry, record who owns it, the NHI discipline the .env era never had to enforce and the agent era can't skip.
Frequently Asked Questions
An MCP (Model Context Protocol) configuration file tells an AI agent which tool servers to run and how to authenticate to them. The common names are .mcp.json (Claude Code), .cursor/mcp.json (Cursor), .vscode/mcp.json (VS Code), and claude_desktop_config.json (Claude Desktop, a user-level file rather than a project one). Unlike .env, the project-level configs are meant to be committed to the repository so a team can share the same agent tooling.
Yes. In our analysis of ~82,000 public MCP config files, 12% of credential slots contained a hardcoded credential literal, including GitHub tokens, database connection strings, cloud keys, and API keys for dozens of SaaS providers. Once committed, these credentials sit in public version control and remain in Git history indefinitely.
Use ${VAR} / ${env:VAR} environment-variable expansion so the value resolves at launch and never enters the repository; on VS Code use ${input:…} so the client prompts for the value; for shared setups reference a secret manager (op://, vault:). Never paste a literal token into the file, and if one was ever committed, rotate it at the provider. Deleting the line does not remove it from Git history.
A non-human identity is a machine credential, an API token, service account, or agent identity, that authenticates software rather than a person. MCP config files are where AI agents' non-human identities are declared, which is why over-privileged, never-expiring, ownerless credentials in these files are an NHI-governance problem, not just a secrets-hygiene one.
What we Built Hush to do
I'll be direct about why we ran this study: the three findings above are the three gaps a scanner alone can't close, and they're the three things we built Hush Security to solve.
Discover every secret in code, including the ones scanners miss. More than half the credentials here have no recognizable token shape (Finding 3), so a value-pattern scanner walks straight past them. Hush's discovery and inventory finds credentials across repositories, secret managers, cloud, Kubernetes, and SaaS by identity, not by regex alone; the internal-MCP bearer token in an Authorization header surfaces right next to the ghp_.
See how every credential is actually used at runtime. A count doesn't tell you blast radius; usage does (Finding 4). Hush's runtime engine correlates each identity with how it authenticates, which permissions it actually exercises, and where a credential is being used right now, turning "org-wide, no expiry, no owner" from an inference into a fact with an accountable owner attached.
Secure the MCP servers themselves, secretlessly. Instead of a long-lived key committed to .mcp.json, the Hush Identity Gateway brokers scoped, just-in-time, revocable access for each agent and the MCP servers it acts through, every call logged, every identity owned, nothing standing. The secret never has to live in the repo, because there is no standing secret to commit.
That is the shift this data argues for: from keeping secrets out of git to governing the non-human identities behind them. Don't let your agents operate in the dark.
If your agents are already reading MCP configs in production, you have non-human identities in version control right now, whether or not you can see them yet. We can help you find them, understand how they're used, and put the Identity Gateway in front of your MCP servers so no standing secret ever has to be committed again. Contact us to get started.
Measurement notes: counts are lower-bound estimates from GitHub code search (default branches, no forks, best-match ordering within size shards). Credentials were classified by pattern and entropy only and never validated; raw values were never stored. Responsible disclosure and legal review precede publication.