Skip to content
Everfinity
← Knowledge

How many API keys do your AI agents hold, and who rotates them?

Security and auditReviewed November 2, 2026

Short answer

Usually every key in the shell you launched it from, plus every key in its config, and in most cases nobody rotates them. We exported 17 fake keys and opened five coding agents. Claude Code, opencode and Qwen Code passed all 17 to a local MCP server. Gemini CLI passed 5 and Codex CLI passed none. 45% of publishers in the official MCP registry ask for at least one more key. Of the 16 keys they ask for most, only three expire by default. Launch agents from a clean environment and give each server its own scoped, expiring key.

We hid 17 fake keys. Three agents gave away every one

Your AI coding agent is probably handing your AWS keys, your GitHub token and your production database password to code you've never read. It doesn't ask, it doesn't log it, and on three of the five agents we tested you can't tell it's happening.

Here's how we found out. Most developers' shells are a key ring. OPENAI_API_KEY in ~/.zshrc, AWS_SECRET_ACCESS_KEY from last year's deploy, a GITHUB_TOKEN for the CLI, a DATABASE_URL that direnv loads on cd. You stop seeing them. Then you open an AI coding agent in that shell, and it starts the MCP servers you installed last month.

We wanted to know which of those keys the servers get. So we built the smallest test we could. Each agent got a throwaway home directory and a shell with 17 fake credentials exported: 12 with names that look like secrets, and 5 with names that don't (POSTGRES_URL, REDIS_URL, MONGODB_URI, SUPABASE_SERVICE_ROLE, OPENROUTER). Every value began with CANARY-, so nothing real was ever exposed. Each agent was configured with the same local MCP server, a few lines of JavaScript that answer the protocol handshake and write down the names of the environment variables they were started with. We also declared one key in the server's own config, the way the setup guides tell you to.

We ran it against five agents at their current versions on 2 November: Claude Code 2.1.316, opencode 1.19.5, Qwen Code 0.26.5, Gemini CLI 0.66.0 and Codex CLI 0.166.0. GitHub's Copilot CLI wouldn't start without a GitHub sign-in, so it isn't in the results. (Google now points new users to its Antigravity CLI, but Gemini CLI is still published on npm.)

Grid of 17 fake secrets against five coding agents. Claude Code 2.1.316, opencode 1.19.5 and Qwen Code 0.26.5 passed all 17 to the MCP server they launched. Gemini CLI 0.66.0 withheld the 12 with secret-looking names, including DATABASE_URL, but passed MONGODB_URI, REDIS_URL, POSTGRES_URL, SUPABASE_SERVICE_ROLE and OPENROUTER. Codex CLI 0.166.0 passed none. All five passed the one key declared in the server's own config.
Each agent started the same local stdio MCP server, which recorded only the names of the variables it received. Our experiment, 2 November 2026. Model calls failed on the fake keys, so no agent could do anything else.

Three of the five agents handed every key to the server. Claude Code, opencode and Qwen Code passed all 17: OpenAI, Anthropic, AWS, GitHub, npm, Stripe, Slack, the database. Gemini CLI held back the 12 with obvious names and passed the 5 without them. Codex passed none. It gave the server only HOME, PATH, TERM and the key we'd declared.

Our server only wrote the names to a file. A real one could just as easily send the values somewhere. That has already happened: in September 2025, postmark-mcp, a server for sending email that had been downloaded 1,643 times, started quietly BCC'ing every message to its author.[1] That one went after email. Running under three of these five agents, it could have taken your AWS keys too, with no extra effort.

Your agent holds all of them, and the protocol said it shouldn't

The honest answer is "all of them". Every key in the environment it was launched from, plus every key in its config files. And here's the strange part: the protocol was designed so this wouldn't happen.

The official MCP SDKs pass a local server exactly six variables by default, HOME, LOGNAME, PATH, SHELL, TERM and USER, from a list the source says was "inspired by the default env inheritance of sudo". Anything else has to be declared for that server explicitly.[2] The spec tells local servers to "retrieve credentials from the environment" rather than use OAuth.[3] It assumes the environment contains what you put there for that server, not everything you've ever exported.

Clients are free to override the default, and three of the five we tested do. This is a design choice, not a bug. Passing your environment through means a GitHub server finds your GITHUB_TOKEN without any setup, and Claude Code's documentation says plugin servers get "access to the same environment variables as manually configured servers".[4] It's convenient. It also means every MCP server you add gets the whole key ring, not just the one key it needs. Each one is a dependency, usually fetched with npx -y at its latest version on every launch, and it runs with your AWS credentials whether it needs them or not. (Our MCP server access page covers what those servers can reach once they're running.)

The one agent that filtered missed the keys that never die

Gemini CLI is the instructive middle case. It redacts variables whose names match patterns such as TOKEN, SECRET, PASSWORD, KEY, AUTH and CREDENTIAL. It blocks a short list of named ones, including DATABASE_URL. And it checks values for the shapes of GitHub, AWS, Stripe and Slack tokens, plus http, https, ftp and smtp URLs that carry a password.[5] We read the filter in the package's source. It is more careful than anything else we tested apart from Codex.

It still passed five of our keys, and they're the ones that matter. postgres://app:…@db/prod isn't an http URL, so the value check misses it, and POSTGRES_URL doesn't contain any of the blocked words. The same goes for mongodb+srv:// and redis://. SUPABASE_SERVICE_ROLE, whose value is the key that bypasses Supabase's row-level security, doesn't contain KEY.

The gap isn't specific to our test. In the official MCP registry, servers mark 12,446 environment variables as secret themselves. 834 of those (6.7%) have names a filter like this lets through: JIRA_PAT, SNOWFLAKE_PAT, SSH_PASSPHRASE, WEBDAV_PASS, LARK_COOKIE, LND_MACAROON_HEX, XRPL_SEED. Some of their values might still match a value pattern. Most won't.

And look at which keys slipped through: database connection strings. When Truffle Security retested credentials that had been committed to public GitHub, 88% of the Postgres connection strings still worked, the highest survival rate of any credential type it measured.[6] The keys a name filter misses are the ones nobody ever rotates.

Even the "safe" agent leaks through the back door

MCP servers are the first way out. The second is the agent's own shell tool, the one that runs npm test and git status when the model asks. On that one, all three agents we checked behave the same way.

Two ways secrets in a developer's shell reach code they did not write. Door one, MCP servers: Claude Code passes all 17 test secrets, Gemini CLI 5 of 17 after filtering by name, Codex CLI none. Door two, the shell tool that runs commands the model requests: all three pass the full environment by default. Gemini CLI's redaction is off by default for shell commands; Codex CLI keeps variables containing KEY, SECRET or TOKEN unless configured otherwise.
Door one was measured with fake keys. For door two, Claude Code was checked by running env in a session, Gemini CLI by reading its source, and Codex CLI from its documented defaults. Our experiment; Codex configuration reference

Codex, the agent that gave our MCP server nothing, runs shell commands with your secrets left in. Its ignore_default_excludes setting, which keeps "variables containing KEY, SECRET, or TOKEN", defaults to true.[7] In Gemini CLI, redaction for shell commands is a setting, enableEnvironmentVariableRedaction, and it is false unless you turn it on. Claude Code's Bash tool runs with the session's full environment. We checked by running env in a session.

So a prompt injection doesn't need a malicious MCP server. It only needs to get the model to run printenv. Attackers worked this out more than a year ago. In August 2025, the malware planted in the Nx build tool didn't only search for secrets itself. It also looked for AI coding CLIs on the machine and prompted them, with their permission checks switched off, to hunt for wallets, keys and .env files. Wiz counted more than a thousand valid GitHub tokens in what was stolen.[8]

MCP servers want your AI bill, and 269 want your crypto wallet

That's the keys an agent holds by accident. Then there are the ones you hand over deliberately, one server at a time.

We downloaded the official MCP registry on 2 November: 54,830 entries, 53,981 of them active.[9] 21,397 are local servers you install and run yourself, from 12,478 publishers. We counted each publisher once, because a single one has listed 1,801 near-identical servers. 5,621 publishers (45%) ask for at least one credential. Astrix's study of 5,200 open-source servers found the same shape: 53% rely on static API keys or personal access tokens, and only 8.5% use OAuth.[10]

Bar chart of how many MCP registry publishers ship a local server that asks for each credential, coloured by expiry policy. Anthropic 149 and OpenAI 147, expiry optional. Google AI or Gemini 111, no expiry. Database URL or password 93, no expiry. GitHub 87, optional. Atlassian 23, expiry required. Brave Search 22, none documented. Figma 21, 90 days maximum. Slack 15, only with rotation turned on. GitLab 15, 365 days by default. AWS access keys 14, cannot expire. Cloudflare 12, optional. Notion 12, none for integration tokens. Stripe 11, no expiry at creation. Hugging Face 7 and Sentry 6, none documented.
A publisher counts once per credential, however many servers it lists. A credential counts when a server's environment variable is flagged secret or named like one. Expiry policy is from each provider's own documentation. MCP Registry, 2 Nov 2026, and provider documentation; our analysis

The top of that chart is our favourite finding. The credential MCP servers ask for most is a key to another AI model. 149 publishers want an Anthropic key, 147 an OpenAI key and 111 a Google one. Install a few of those servers and your model bill sits behind several more copies of a model key, each in its own config file, each one able to spend your money.

The strangest line isn't on the chart. 269 servers, from 231 publishers, ask for a crypto wallet's private key or seed phrase: EVM_PRIVATE_KEY, SOLANA_PRIVATE_KEY, X402_BUYER_PRIVATE_KEY, ALGORAND_MNEMONIC. That's a credential you can't rotate. The only way to "rotate" a seed phrase is to move the money to a new wallet. These servers let an agent pay for things, and every one of them receives every other key in your shell too.

Who rotates them? Nobody. 88% of leaked database URLs still work

Now the second half of the question. Look at the colours on that chart. Of the 16 credentials MCP servers ask for most, only three expire unless someone sets them to: Atlassian tokens (1 to 365 days, required), Figma (90 days at most) and GitLab (365 days by default).

The rest wait for a person to act. Anthropic offers expiries from "3 hours" to "Never". The choice "is set at creation time and cannot be changed afterward", so a key someone created without an expiry in 2024 keeps that setting.[11] Classic GitHub tokens "do not have an expiration requirement" unless an organisation imposes one.[12] AWS access keys can't expire at all, and AWS's own advice is to use temporary credentials instead.[13] Google's AI Studio keys carry no expiry date. Google's answer since May 2026 has been to make new keys service-account-bound "auth keys" with faster enforcement when they leak.[14]

So the honest answer to "who rotates them" is: nobody does, until one leaks. And they leak. GitGuardian found 24,008 unique secrets in MCP configuration files pushed to public GitHub, 2,117 of which were confirmed valid. Of credentials it had confirmed valid in 2022, more than 64% still worked in January 2026.[15]

Bar chart of the share of credentials committed to public GitHub that still authenticated in July 2026. Postgres connection URIs 88 percent, 11,465 of 12,985. MySQL 75 percent. Google Cloud service accounts 54 percent. SendGrid 40 percent. Docker Hub 33 percent. AWS access keys 8 percent. Stripe 4 percent. Redis URLs 3 percent. Slack 2 percent. GitLab 0.64 percent. GitHub tokens 0.36 percent, Hugging Face 0.05 percent and npm 0.001 percent, 1 of 101,886, the providers with a pipeline that kills leaked tokens.
Credentials found in public GitHub code and tested in July 2026. The median one had been public for 784 days. Truffle Security, September 2026

Truffle's numbers show what actually decides whether a leaked key keeps working, and it isn't how careful the owner is. It's whether the provider kills leaked keys for you. Of 101,886 npm tokens committed to public code, one still worked. Of 73,048 GitHub tokens, 260. Hugging Face goes further: anyone who finds a leaked token can revoke it through a public endpoint, without access to the account it belongs to.[16] Database connection strings have no provider to do that. Nobody gets an alert and nobody revokes them, which is why 88% of the Postgres ones still opened a connection.[6]

Put the findings together. The keys a name filter misses are database URLs. The keys nobody revokes are database URLs. And 93 MCP publishers ask you to paste one into a config file.

Is your agent doing this? Find out in two minutes

Add a throwaway "server" that writes down which variable names it was started with, never their values. For Claude Code:

claude mcp add envcheck -- sh -c 'env | cut -d= -f1 | sort > "$HOME/mcp-env.txt"'
claude mcp list
grep -icE 'key|token|secret|pass|auth|cred|url|uri' ~/mcp-env.txt
claude mcp remove envcheck

claude mcp list will report that envcheck failed to connect. That's expected: it never answers, it just takes notes. The grep prints how many secret-looking names reached it, and you can drop the c from -icE to see the names. In our lab shell it found 15 of the 17. It missed SUPABASE_SERVICE_ROLE and OPENROUTER, which is the point of the previous section. For other agents, put the same sh -c command in their MCP config and start them once.

Then fix it. The quickest fix we tested is to launch the agent from a clean environment:

env -i HOME="$HOME" PATH="$PATH" TERM="$TERM" claude

Launched that way, our probe server got 0 of the 17 shell secrets and still received the key declared in its own config. Two caveats. The agent has to sign in through its own login or keychain, not an exported API key, because any key you pass through env -i is back in the environment, and three of these agents will hand it on. And on some machines you'll want to add SSH_AUTH_SOCK or a proxy variable back in. Longer term, stop exporting secrets in your shell profile. Inject them per process with your secret manager's run command, so each key exists only in the process that needs it.

What to actually do on Monday

  1. Run the check on every agent your team uses. It takes two minutes, and the answer changes between agents, and between versions of the same agent.
  2. Empty your shell profile of secrets. Anything exported in ~/.zshrc or loaded by direnv reaches every agent, every MCP server and every command the model runs.
  3. Give each MCP server its own key. A fine-grained GitHub token for one repository, a restricted Stripe key, a Postgres role that can only read. One server, one key, one blast radius.
  4. Set an expiry on every key that allows one. Then set an organisation-wide maximum where the provider supports it (OpenAI, Anthropic and GitHub do), so the next key someone creates can't be set to "never".
  5. Prefer remote servers with OAuth over local servers that want a static key. The spec forbids an MCP server from passing on the token it receives, and an OAuth grant can be revoked in one place.[3]
  6. Never give an agent a key you can't rotate. That means wallet seeds, root AWS keys and the database superuser password. If losing it would mean moving money or rebuilding a database, keep it away from agents.
  7. Write the inventory. One line per key: which agent or server holds it, which file it lives in, when it expires and who rotates it. If the last column has a name in it, you're ahead of most teams.

The one-line version

Ask how many keys your AI agent holds and the answer is usually "everything in the shell it started in". Three of the five agents we tested passed all 17 of our fake keys to an MCP server. Almost half of MCP publishers ask for at least one more. Of the 16 keys they ask for most, only three expire by default. Nobody rotates them until one leaks, and the database URLs don't get rotated even then. If you want help finding every key your agents can reach, that's part of what the readiness audit covers. And if an agent can install packages, the slopsquatting page explains what it can bring in.

Follow-up questions

Do MCP servers get my environment variables?
It depends on the client that launches them, not on the server. The official MCP SDKs pass a local server only six variables by default (HOME, LOGNAME, PATH, SHELL, TERM and USER) plus whatever its config declares. In our test, Claude Code, opencode and Qwen Code passed the full environment instead, so every secret exported in the shell reached the server. Codex CLI passed nothing beyond the allow-list, and Gemini CLI filtered by variable name.
Which AI coding agents pass my shell's secrets to MCP servers?
With 17 fake keys exported, Claude Code 2.1.316, opencode 1.19.5 and Qwen Code 0.26.5 passed all 17 to a local MCP server. Gemini CLI 0.66.0 withheld the 12 with names like OPENAI_API_KEY, but passed five whose names don't look secret, such as POSTGRES_URL and REDIS_URL. Codex CLI 0.166.0 passed none. All five passed the key declared in the server's own config.
How many credentials do MCP servers ask for?
Of 12,478 publishers with a local server in the official MCP registry, 5,621 (45%) ask for at least one credential. The most requested are keys for other AI models: Anthropic (149 publishers), OpenAI (147) and Google AI or Gemini (111). Next come database URLs (93) and GitHub (87). 269 servers ask for a crypto wallet's private key or seed phrase.
Do the API keys AI agents use expire?
Mostly not unless someone sets it. Of the 16 credentials MCP servers ask for most, only Atlassian, Figma and GitLab tokens expire by default. Anthropic, OpenAI, GitHub, Cloudflare and Slack let you set an expiry or rotation, but don't by default. AWS access keys cannot expire at all, and Google AI keys, Brave, Notion integration tokens, Stripe, Hugging Face and Sentry document no expiry setting.
How do I stop my AI agent from leaking keys to MCP servers?
Launch the agent from a clean environment, for example env -i HOME="$HOME" PATH="$PATH" TERM="$TERM" claude, and declare each server's key in that server's own config. In our test, that took a local MCP server from 17 fake keys to the one we declared. Keep secrets out of your shell profile, give each server its own scoped key with an expiry, and remember the agent's shell tool is a second way out.

Sources

  1. First malicious MCP server found stealing emails in rogue Postmark-MCP packageThe Hacker News · 2025-09-29
  2. client/stdio.ts: DEFAULT_INHERITED_ENV_VARS and getDefaultEnvironment()Model Context Protocol TypeScript SDK (GitHub) · 2026-11
  3. Authorization (specification version 2025-11-25)Model Context Protocol · 2025-11-25
  4. Connect Claude Code to tools via MCPClaude Code documentation (Anthropic) · 2026-11
  5. MCP servers with the Gemini CLIGemini CLI documentation (Google) · 2026
  6. GitHub repos exposed 543,699 credentials. Nobody revoked them.Truffle Security · 2026-09-29
  7. Configuration referenceOpenAI Codex documentation · 2026-11
  8. s1ngularity: supply chain attack leaks secrets on GitHubWiz Research · 2025-08-27
  9. MCP Registry API, all server entriesModel Context Protocol Registry · 2026-11-02
  10. State of MCP Server Security 2025Astrix Security · 2025-10-15
  11. Authentication: API keysClaude Platform documentation (Anthropic) · 2026-11
  12. Token expiration and revocationGitHub Docs · 2026-11
  13. Manage access keys for IAM usersAWS Identity and Access Management User Guide · 2026-11
  14. Using Gemini API keysGoogle AI for Developers · 2026-11
  15. The State of Secrets Sprawl 2026GitGuardian · 2026-03-17
  16. User access tokens: revoking a leaked tokenHugging Face Hub documentation · 2026-11

Apply this to your own setup

The free readiness audit asks seven questions and returns a costed plan — the same reasoning as this page, against your numbers.

Disagree, or have a harder case?

Twenty minutes with the person who would build it. Bring the version of this problem that doesn't fit the page.