MCP-004 Inline credential in env block
MCP host configs frequently inline live API keys
(OPENAI_API_KEY: sk-..., GITHUB_TOKEN: ghp_...) in a server's
env block rather than referencing a shell variable (${VAR}).
The correct shape is "the host resolves the value from the OS env
at launch"; the wrong shape is "the value lives in this JSON file
forever, alongside your other dotfiles, in your machine's backup,
and in the directory you sync to a cloud service".
What Vulkro detects
Rule fires when a server's env map contains an entry where every
gate passes:
- The key contains a credential-name lemma (
AWS_ACCESS_KEY_ID,GITHUB_TOKEN,STRIPE_API_KEY,SLACK_BOT_TOKEN, etc.; the substring catalog is shared with the secrets detector). - The value is not empty.
- The value is NOT a
${VAR}/$VARenv-indirection reference. - Optionally: the value matches a provider-format regex (AWS, Stripe, GitHub, Slack, PEM private key, JWT, etc.).
Severity stratification:
- Critical: name catalog hit AND provider-format match. The value is a literal production credential.
- High: name catalog hit AND value length >= 16 (plausible custom token without a well-known prefix).
- Medium: name catalog hit only, short or opaque value.
Confidence: High when the provider-format regex hit, Medium
otherwise.
Evidence signals: mcp-env-secret-name-match (weight 0.5,
Pattern) always, plus mcp-env-secret-provider-format-match
(weight 0.7, Pattern) when the value matches a known provider
format.
Privacy guarantee: Vulkro never embeds the secret value in
the finding message or evidence detail. The detail reports the key
name and a coarse length bucket (<8, 8-15, 16-23, 24-31,
32-39, 40-47, 48-63, 64-127, 128+) only. Single-digit
length precision would itself fingerprint the value; the bucketing
is deliberate. The framing is "you committed this", not "we saw
this".
Non-compliant config
{
"mcpServers": {
"github": {
"command": "uvx",
"args": ["mcp-server-github==0.4.2"],
"env": {
"GITHUB_TOKEN": "ghp_3rL9aXk2qNvT8WzC0xY1fE6sBpH4dM7uV5jK"
}
},
"stripe": {
"command": "npx",
"env": {
"STRIPE_API_KEY": "sk_live_51N8t7K2eZvB4cQfL9xA0pY3uT6wM8d"
}
}
}
}
Both values are literal production credentials sitting in a JSON
file under ~/.cursor/ or ~/Library/Application Support/Claude/.
Compliant config
{
"mcpServers": {
"github": {
"command": "uvx",
"args": ["mcp-server-github==0.4.2"],
"env": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
}
},
"stripe": {
"command": "npx",
"env": {
"STRIPE_API_KEY": "${STRIPE_API_KEY}"
}
}
}
}
The shell sets GITHUB_TOKEN and STRIPE_API_KEY (in ~/.zshrc,
~/.bashrc, a per-project direnv file, or a platform secrets
manager); the host process reads them from the OS env at launch.
Remediation
- Replace the inline literal with a
${VAR}reference. The MCP host resolves the variable from the process environment at launch, so the JSON file no longer carries the secret. - Set the variable somewhere appropriate: a shell rc file for
personal credentials, a
direnvconfig for per-project scoping, your OS keychain or a platform secrets manager for shared workstations. - Rotate the credential if it was a real one. The literal now exists in the host config file's git history (if the file is tracked) and in every backup the machine has taken since you added it. Rotation is the only way to invalidate copies you cannot reach.
See also
vulkro mcp-audit- parent CLI command.- Secrets detection - the broader secrets pipeline that powers the credential-name lemma catalog.
- Confidence model - what
High,Medium,Lowmean.