MCP configs are the new .env files. I scanned GitHub and found live database keys committed in public repos.
If you use Claude Desktop, Claude Code, Cursor or Windsurf with external tools, you have an .mcp.json or claude_desktop_config.json file on your machine. It tells your editor which MCP servers to start, and each server entry can carry an env block.
That env block is a secret store. And developers are pasting real keys into it — then committing the file.
What I found
I ran GitHub code search for filename:.mcp.json SUPABASE_SERVICE_ROLE and similar queries, then verified every result instead of trusting the match:
-
Decode, don't pattern-match. A file only counts if it contains a JWT whose decoded payload literally says
"role": "service_role". In my scan, 53 of 65 candidate files were placeholders, templates or examples. If I had emailed all 65, four out of five would have been noise — the fastest way to teach someone to ignore security reports. -
Probe whether the project is live. A leaked key for a deleted Supabase project is worthless. A
GET https://<ref>.supabase.co/rest/v1/that answers401(no credentials sent, nothing read) proves the project is live and the key is valid until rotated.
The result: 12 files in public repos contain a real, decodable service_role key. 4 of those projects are live right now. One of them was pushed the same day I scanned.
A service_role key bypasses every row-level-security policy. Whoever finds it has full read/write access to the database — not "some" access, all of it.
Why this keeps happening
The mental model is wrong, not the people. .mcp.json feels like editor configuration — like settings.json, like .vscode/. Nobody expects a linter or a framework to warn them, because no tool in this ecosystem treats the file as a secret store yet.
Compare with .env: a decade of leaked keys produced pre-commit hooks, secret scanners, CI checks, and platform warnings. .mcp.json has none of that guardrail history, so the same 2015-style mistakes are being made again in 2026 — just with database keys that grant more access than most API keys ever did.
Deleting the file doesn't help either: git history keeps every version forever. The only fix that works is rotating the key (Supabase Dashboard → Settings → API → rotate), then moving the new value into a runtime environment variable.
I built the linter I wanted to exist
I've spent months doing Supabase RLS audits. The configuration side of this problem needs zero-expertise tooling, so I built a family of zero-dependency auditors. Each one runs locally, reads nothing from the network, and redacts evidence in its reports so the report itself is safe to paste into an issue:
-
mcp-config-security —
npx mcp-config-security. Scans Claude Desktop, Claude Code, Cursor and Windsurf configs for service_role JWTs (decoded, so a token hiding under a neutral key name is still caught), real-looking tokens inenv/headers,--dangerously-skip-permissionsflags, silentautoApproveon write tools, plaintexthttp://remotes, disabled TLS verification, and filesystem servers rooted at/. -
supabase-service-role-scanner —
npx supabase-service-role-scanner. Walks a project forNEXT_PUBLIC_*SERVICE_ROLE*variables,NEXT_PUBLIC_values carrying non-anon JWTs, and service_role literals in source. Theanonkey inNEXT_PUBLIC_is correct by design and never flagged. -
github-actions-audit —
npx github-actions-audit. Static scan of workflow YAML forpull_request_targetpwn requests,${{ github.event.pull_request.title }}-style shell injection, hardcoded secrets, and unpinned actions. -
firebase-rules-audit —
npx firebase-rules-audit. Findsallow read, write: if true;,request.auth == nullgrants and conditions with no auth check at all, in Firestore and Storage rules. -
n8n-workflow-audit —
npx n8n-workflow-audit. Exported workflow JSON often carries tokens pasted straight into HTTP Request nodes. This flags hardcoded credentials,?apikey=in URLs and unauthenticated webhooks — while treating n8n's owncredentials: { id, name }references as the correct pattern.
All free, all MIT, no accounts, no telemetry. The companion repo supabase-rls-leak-demo covers the database side: a fixture that goes red on a broken RLS policy and green on the one-file fix.
My own tool flagged my own CIs
The first thing I ran against my own four repos was github-actions-audit. It came back with 20 findings: every actions/checkout@v4 and actions/setup-node@v4 was a mutable tag, and none of the workflows had a permissions: block.
That is the correct experience for this class of tool — it should be slightly embarrassing. I resolved the action tags to their commit SHAs (git.ref/tags/v4), pinned all four workflows, added permissions: contents: read, and pushed. Same command now exits 0. If a security linter finds nothing on its first run against your config soup, be suspicious of the linter.
What to do tonight
If you have an .mcp.json or claude_desktop_config.json anywhere in a repo — or a NEXT_PUBLIC_ Supabase variable you're not sure about:
npx mcp-config-security
npx supabase-service-role-scanner .
Two commands, a few seconds, zero installs. If they come back clean, you're done. If they don't, rotate first, refactor second.
I reported the live leaks I found privately — no bounties asked, just the rotation. If one of these tools catches something real for you, you can buy me a coffee, but the tools stay free either way.
Top comments (3)
Agreed on all of it — treat them exactly like .env files.
Two small additions from the trenches:
gitignore alone isn't enough because the config usually has to exist for the agent to run at all. The pattern that survives is .mcp.json as a redacted template plus a gitignored local override (.mcp.local.json) holding real values — most clients honor a local-override chain, and the "teammate clones the repo at 2am" case, which is where most of these leaks actually originate, gets the template instead of the keys.
For the pre-commit hook, generic secret scanners (gitleaks & co.) catch the common token formats, but Supabase JWTs need actual payload decoding to tell a real service_role key from a placeholder that looks like one — in the batch I scanned, 53 of 65 candidate files were fakes that a pattern-matcher would have flagged. That's the difference between a scanner your team trusts and one they learn to ignore.
Treat MCP config files like application secret files. Keep real credentials out of version control, commit a redacted example, and inject secrets at runtime from a password manager or secret store. Add config filenames and local overrides to .gitignore, but also run secret scanning in pre-commit and CI because ignore rules do not protect files that are already tracked. If a key reaches GitHub, removing the commit is not enough. Revoke or rotate it immediately, review the provider’s audit logs, and assume any high-privilege token may already have been copied. Least-privilege, short-lived credentials also limit the damage if a config leaks.
The decode-don't-pattern-match step is what separates a report from noise - 53 of 65 being placeholders is a humbling ratio. Every new tool that reads a local config with an env block is a new place secrets can leak from, and gitignore habits haven't caught up.