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.)
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.
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]
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]
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
- 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.
- Empty your shell profile of secrets. Anything exported in
~/.zshrcor loaded bydirenvreaches every agent, every MCP server and every command the model runs. - 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.
- 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".
- 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]
- 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.
- 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
- First malicious MCP server found stealing emails in rogue Postmark-MCP packageThe Hacker News · 2025-09-29
- client/stdio.ts: DEFAULT_INHERITED_ENV_VARS and getDefaultEnvironment()Model Context Protocol TypeScript SDK (GitHub) · 2026-11
- Authorization (specification version 2025-11-25)Model Context Protocol · 2025-11-25
- Connect Claude Code to tools via MCPClaude Code documentation (Anthropic) · 2026-11
- MCP servers with the Gemini CLIGemini CLI documentation (Google) · 2026
- GitHub repos exposed 543,699 credentials. Nobody revoked them.Truffle Security · 2026-09-29
- Configuration referenceOpenAI Codex documentation · 2026-11
- s1ngularity: supply chain attack leaks secrets on GitHubWiz Research · 2025-08-27
- MCP Registry API, all server entriesModel Context Protocol Registry · 2026-11-02
- State of MCP Server Security 2025Astrix Security · 2025-10-15
- Authentication: API keysClaude Platform documentation (Anthropic) · 2026-11
- Token expiration and revocationGitHub Docs · 2026-11
- Manage access keys for IAM usersAWS Identity and Access Management User Guide · 2026-11
- Using Gemini API keysGoogle AI for Developers · 2026-11
- The State of Secrets Sprawl 2026GitGuardian · 2026-03-17
- User access tokens: revoking a leaked tokenHugging Face Hub documentation · 2026-11