API Key Plaintext in Agents Is a Liability
Hermes user built OpenPass for secure credential management. Problem: .env files in agent repos with no rotation or audit trail. Solutions and architecture.
Storing API keys in .env files within agent repositories is the default pattern in 2026, and it is a security liability. No rotation, no audit trail, no scoping, no revocation without redeployment. When agents run autonomously and make decisions about which tools to call, unmanaged credentials become an attack surface that scales with your agent count.
The problem with .env files in agent repos
A typical agent project has a .env file with 5-15 API keys. These keys are copied between environments, shared over Slack, committed to private repos (which get leaked), and never rotated. When one agent is compromised via prompt injection, every service connected to that agent is exposed. There is no way to know which agent used which key for what purpose without manual log correlation.
# The typical .env file in an agent project
OPENAI_API_KEY=sk-proj-abc123...
SCAVIO_API_KEY=sk-live-def456...
ANTHROPIC_API_KEY=sk-ant-ghi789...
TAVILY_API_KEY=tvly-jkl012...
DATABASE_URL=postgres://admin:password@prod-db:5432/main
STRIPE_SECRET_KEY=sk_live_mno345...
# Problems:
# - No rotation schedule
# - Same key used by all agents
# - No per-agent spending limits
# - No audit trail of which agent called what
# - Committed to repo history even if .gitignored laterSolution 1: Environment variable injection at runtime
Never store secrets in files that live alongside code. Inject them at runtime from your deployment platform (Railway, Fly.io, AWS ECS) or a secrets manager. The agent process receives credentials in memory only -- nothing on disk to leak.
import os, subprocess
def start_agent_with_injected_secrets(agent_name: str):
"""Start agent with secrets injected from vault, not from .env."""
secrets = fetch_from_vault(agent_name)
env = os.environ.copy()
env.update(secrets)
subprocess.run(
["python", "agent.py"],
env=env,
check=True,
)
def fetch_from_vault(agent_name: str) -> dict:
"""Fetch only the secrets this specific agent is authorized to use."""
import hvac
client = hvac.Client(url=os.environ["VAULT_ADDR"])
secret = client.secrets.kv.v2.read_secret_version(
path=f"agents/{agent_name}"
)
return secret["data"]["data"]Solution 2: MCP credential servers
Instead of giving agents raw API keys, give them access to a credential MCP server that handles authentication on their behalf. The agent never sees the actual key -- it calls a tool, and the MCP server attaches the credential server-side.
// MCP credential proxy pattern
// Agent calls "search" tool -> MCP server adds the API key server-side
{
"mcpServers": {
"credentialed-search": {
"command": "node",
"args": ["credential-proxy-mcp.js"],
"config": {
"upstream": "https://api.scavio.dev/api/v1/search",
"credentialSource": "vault://secret/scavio/production",
"rotationInterval": "7d",
"auditLog": "s3://logs/mcp-audit/"
}
}
}
}
// Agent never sees "sk-live-..." -- it just calls the search tool
// Credential proxy handles: injection, rotation, logging, rate limitingSolution 3: Short-lived scoped tokens
Instead of long-lived API keys, generate short-lived tokens scoped to specific operations. A research agent gets a token valid for 1 hour that can only call search endpoints. A write agent gets a token valid for 10 minutes that can only modify specific resources.
import jwt, time
def generate_agent_token(agent_id: str, allowed_tools: list, ttl_minutes: int = 60):
payload = {
"agent_id": agent_id,
"tools": allowed_tools,
"exp": int(time.time()) + (ttl_minutes * 60),
"iat": int(time.time()),
"max_credits": 100, # Budget cap per token
}
return jwt.encode(payload, os.environ["TOKEN_SECRET"], algorithm="HS256")
# Research agent gets limited, short-lived access
token = generate_agent_token(
agent_id="research-agent-01",
allowed_tools=["search", "serp"],
ttl_minutes=60,
)Why this matters for production agents
- Agent count is scaling: teams run 10-50 agents, each needing credentials
- Prompt injection is real: a compromised agent with long-lived keys is an open door
- Compliance requires audit trails: you need to prove which agent accessed what and when
- Cost control requires scoping: one runaway agent should not drain your entire budget
- Key rotation without downtime: you cannot restart 50 agents to rotate one key
Migration path from .env to secure credentials
- Inventory all API keys across all agent .env files
- Move keys to a secrets manager (Vault, AWS Secrets Manager, Doppler)
- Create per-agent credential scopes with budget limits
- Replace .env reads with vault fetches at startup
- Add audit logging to every credential access
- Set up automated rotation with zero-downtime key swaps
- Delete .env files and purge from git history
The cost of proper credential management is hours of setup. The cost of a credential leak in a production agent fleet is unbounded. Start with vault injection and audit logging -- those two changes eliminate the worst failure modes.