AI agents are helpful, not malicious. That's what makes them dangerous around secrets. Here's an MCP server that catches secret exposure before the agent gets there.
Here's a scene that's more common than anyone admits.
You're debugging a config issue. You ask your AI agent to look at src/config.ts. The file has this:
export const config = {
db: { url: process.env.DATABASE_URL, password: process.env.DB_PASSWORD },
jwt: { secret: process.env.JWT_SECRET },
};
// Added during debugging last Tuesday, never removed
console.log("Config loaded:", JSON.stringify(config));
console.log(process.env.AWS_SECRET_ACCESS_KEY);
The agent reads the file, executes the tool, and your AWS secret key is now sitting in its context window. It might summarize it. It might include it in generated code. It might pass it to another tool that logs everything.
None of this requires the agent to be malicious. It just needs to be helpful.
env-secret-exposure-analyzer-mcp catches this before it happens. 🔐
🙈 The three ways secrets leak
1. Hardcoded in source files
The classic. Someone puts a token directly in code "just to test" and commits it. Or copies a .env value into a constant because it's easier. Or the most common one: a connection string with the password embedded right there in the URL.
DATABASE_URL=postgres://admin:p@ssw0rd123@prod.db.internal:5432/app
An agent reading any file that imports this config now has your prod database password.
2. .env not in .gitignore
Your .env has 40 keys. AWS credentials, Stripe keys, JWT secrets, OAuth tokens, encryption keys. It's not in .gitignore. One git push and it's in the repository forever — even if you delete it, it stays in git history.
The agent doesn't know this is dangerous. It reads files. That's its job.
3. console.log that never got removed
The debug line that gets committed on Friday and nobody notices until Monday. Except by then, every time the app starts, it dumps credentials to stdout. If you have log aggregation, those secrets are now in your observability platform too.
console.log("Starting server with config:", JSON.stringify(config));
// config contains { db: { password: "..." }, jwt: { secret: "..." } }
🔧 How the scanner works
Three tools, three attack surfaces.
scan_for_secrets
Scans source files, config files, and .env files for 20+ patterns. Returns severity, file, line, and a masked preview — the scanner never returns the full secret value, even in its own output.
After building the initial version with the obvious patterns (AWS, GitHub, Stripe), we did something simple: created a realistic .env with everything a real project might have and ran the scanner against it.
First run: 3 findings.
That's not good enough. A real .env has database URLs, JWT secrets, SendGrid keys, Twilio tokens, Google OAuth secrets, Sentry DSNs, webhook secrets, encryption keys. We were missing most of it.
We fixed the patterns:
- Database URLs with embedded credentials — had to handle passwords containing
@(the character that separates credentials from host) - Stripe webhook secrets (
whsec_), Google OAuth (GOCSPX-), Sentry DSN with flexible key length - Generic patterns for JWT secrets, session secrets, encryption keys — with a
(?!process\.env)lookahead to avoid false positives onpassword: process.env.Xwhich is actually correct code
After fixes: 16 findings. Everything in the file.
check_gitignore_coverage
Walks the project root and checks if sensitive files (.env, .env.local, secrets.json, private keys, certificates) are covered by .gitignore. Correctly ignores .env.example and .env.sample — those are supposed to be committed.
scan_for_log_leaks
Scans for console.log / logger calls that print process.env variables or objects with secret-sounding names. Catches both direct leaks:
console.log(process.env.AWS_SECRET_ACCESS_KEY); // HIGH
And indirect ones:
console.log("Config:", JSON.stringify(config)); // HIGH — config contains env vars
console.log("env:", process.env); // CRITICAL — dumps everything
🐸 The most ironic moment of this build
When writing the tests, I created fixture files with fake-but-realistic secrets:
GITHUB_TOKEN=ghp_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AWS_ACCESS_KEY_ID=AKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
GitHub push protection blocked the push.
A secret scanner — blocked by GitHub's secret scanner — because its own test fixtures looked too much like real secrets. Even the obviously fake ones. ghp_ followed by 40 A's is still ghp_ followed by 40 alphanumeric characters, which is exactly the GitHub token format.
The fix: create fixture files programmatically in os.tmpdir() at test time. They never touch the git repo. The tests pass, the push goes through, and somewhere in the universe a security engineer nods approvingly.
beforeAll(() => {
leakyDir = fs.mkdtempSync(path.join(os.tmpdir(), "env-mcp-leaky-"));
fs.writeFileSync(
path.join(leakyDir, ".env"),
[`GITHUB_TOKEN=ghp_${"A".repeat(40)}`, `AWS_ACCESS_KEY_ID=AKIA${"A".repeat(16)}`].join("\n"),
);
});
afterAll(() => {
fs.rmSync(leakyDir, { recursive: true, force: true });
});
✅ Does it actually work?
After publishing, I verified the full MCP protocol layer — not just the functions in isolation, but the actual stdio transport that Claude Desktop uses:
printf '{"jsonrpc":"2.0","id":1,"method":"initialize",...}\n
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"scan_for_secrets",...}}\n' \
| node dist/index.js
Response:
{
"result": {
"content": [{
"type": "text",
"text": "Secret Scan Results\n Files scanned: 9\n Findings: 16\n\n [CRITICAL] .env:7 — Database URL with password\n [CRITICAL] .env:17 — AWS Access Key\n ..."
}]
},
"jsonrpc": "2.0",
"id": 2
}
Error handling too — bad path returns "isError": true with a readable message, doesn't crash the server.
For a security tool, "it compiles" is not good enough. The protocol has to work.
⚡ Setup
{
"mcpServers": {
"secret-scanner": {
"command": "npx",
"args": ["-y", "env-secret-exposure-analyzer-mcp"]
}
}
}
Then ask your agent:
"Scan this project for exposed secrets, check if .env is in .gitignore, and find any console.log calls that might be leaking credentials."
🐸 The pattern
Every tool in this series exists because an AI agent hits a wall — something it structurally cannot know without the right tool.
With secrets, the wall is subtle. The agent doesn't know what's sensitive just by looking. It doesn't know p@ssw0rd123 in a connection string is a production password. It doesn't know that JWT_SECRET=abc123verysecretkey in a .env file means something if that file gets committed.
The scanner knows. It has the patterns, the gitignore logic, the log analysis. It tells the agent exactly what's dangerous before the agent gets near it.
Top comments (0)