Running autonomous agents that interact with multiple AI providers means juggling a lot of API keys. Each one is a potential leak surface. Hermes Agent’s credential guard system is designed to prevent those leaks.
The Problem: Credential Bleed
When an agent runs a multi-step task involving different providers, the naive approach loads all credentials into a shared environment. This means a tool call meant for OpenAI can accidentally expose your Anthropic key, or a compromised local model handler can read credentials it shouldn’t have access to.
Threat model: what “credential bleed” actually enables
| Attack Vector | Prerequisite | Impact | Real-World Example |
|---|---|---|---|
| Cross-provider key reuse | Agent loads all keys into process.env |
Anthropic key used for OpenAI call (billing fraud, rate limit burn) | Silent quota/model burn |
| Tool handler compromise | Malicious/buggy tool reads process.env |
Full key exfiltration to attacker-controlled endpoint | GhostCommit PNG injection |
| Log leakage | Structured logging serializes env | Keys in CI logs, observability platforms | Zero sandbox credential leak |
| Subagent inheritance | Parent spawns child with full env | Subagent inherits keys it doesn’t need | Claude Code subagent prompt injection |
The root cause is ambient authority: credentials available to the process become available to every component, regardless of need.
The Solution: Shared Read Guard
Hermes routes all credential access through a single chokepoint. Every provider call must pass through this gate, which validates the caller’s identity, checks authorization, logs every credential read with caller context, and routes the credential directly into the request — never exposing it to intermediate handlers:
// Core credential guard interface (simplified)
interface CredentialGuard {
read(request: CredentialRequest): Promise<string>;
auditLog: AuditEntry[];
}
interface CredentialRequest {
provider: ProviderId; // 'openai' | 'anthropic' | 'google' | ...
caller: string; // Tool/component identifier
purpose: 'user-request' | 'background' | 'tool-invocation';
scope?: 'task' | 'session' | 'global';
}
const credentialGuard: CredentialGuard = {
async read({ provider, caller, purpose, scope = 'task' }) {
// 1. Validate caller is registered and authorized for this provider
const policy = await policyStore.get(provider, caller);
if (!policy?.allows(purpose, scope)) {
auditLog.push({ provider, caller, purpose, scope, result: 'DENIED', timestamp: Date.now() });
throw new CredentialAccessError(`Caller ${caller} not authorized for ${provider}`);
}
// 2. Retrieve credential from secure store (not process.env)
const credential = await secureStore.get(provider);
if (!credential) {
throw new CredentialNotFoundError(`No credential configured for ${provider}`);
}
// 3. Audit the successful read
auditLog.push({ provider, caller, purpose, scope, result: 'GRANTED', timestamp: Date.now() });
// 4. Return credential directly — never touches process.env
return credential;
},
auditLog: []
};
Usage in provider adapters
// Provider adapter — ONLY way to get credentials
class OpenAIAdapter {
async complete(params: CompletionParams) {
const apiKey = await credentialGuard.read({
provider: 'openai',
caller: 'openai-completion-tool',
purpose: 'user-request'
});
// Key injected directly into request, never in scope
return fetch('https://api.openai.com/v1/chat/completions', {
headers: { Authorization: `Bearer ${apiKey}`, ... },
body: JSON.stringify(params)
});
}
}
Local Provider Input Protection
The guard system also handles local input routing. When an image or file is processed locally, the guard ensures local handlers never see remote credentials, and vice versa. Recent commits hardened this further by routing all local image-gen inputs through the same shared guard chokepoint.
Isolation guarantee
┌─────────────────────────────────────────────────────────────┐
│ AGENT PROCESS │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ OpenAI │ │ Anthropic │ │ Local (Ollama) │ │
│ │ Adapter │ │ Adapter │ │ Image Handler │ │
│ └──────┬──────┘ └──────┬──────┘ └────────┬────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ CREDENTIAL GUARD (chokepoint) │ │
│ │ • Validates caller identity │ │
│ │ • Checks policy (provider ↔ caller ↔ purpose) │ │
│ │ • Retrieves from secure store (NOT process.env) │ │
│ │ • Audits every access │ │
│ │ • Returns credential → request ONLY │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ OpenAI API Anthropic API Local GPU │
└─────────────────────────────────────────────────────────────┘
Key invariant: The local image handler cannot call credentialGuard.read({ provider: 'openai', ... }) — the policy rejects it. The OpenAI adapter cannot access local file paths without explicit tool permission. Cross-domain leakage is prevented by policy, not convention.
File-Based Credential Detection
Beyond runtime guards, Hermes includes a static credential scanner for its own configuration files — scanning .env for exposed keys with case-insensitive matching, blocking .env files from the managed-files API entirely, and warning on startup if credential file permissions are too permissive.
Scanner implementation
// Case-insensitive secret pattern matching
const SECRET_PATTERNS = [
/^(?i)(api[_-]?key|secret|token|password|passwd|auth[_-]?key)$/,
/^(?i)(aws[_-]?access[_-]?key|aws[_-]?secret[_-]?key)$/,
/^(?i)(github[_-]?token|gh[_-]?pat)$/,
/^(?i)(anthropic[_-]?api[_-]?key|openai[_-]?api[_-]?key|google[_-]?api[_-]?key)$/,
];
function scanEnvFile(content: string): ScanResult[] {
const findings: ScanResult[] = [];
for (const line of content.split('\n')) {
const match = line.match(/^([^=]+)=(.+)$/);
if (!match) continue;
const [, key, value] = match;
for (const pattern of SECRET_PATTERNS) {
if (pattern.test(key.trim())) {
findings.push({ key: key.trim(), severity: 'HIGH', line });
break;
}
}
}
return findings;
}
Managed-files API block
// .env files are explicitly denied from the file management API
const BLOCKED_PATHS = ['.env', '.env.*', '*.pem', '*.key', 'id_rsa*', '*.kdbx'];
async function canManageFile(path: string): boolean {
return !BLOCKED_PATHS.some(pattern => minimatch(path, pattern));
}
Startup permission check
# On startup, Hermes checks .env permissions
# Warns if: world-readable, group-writable, or owned by different user
if [[ $(stat -c '%a' .env 2>/dev/null) =~ ^[0-7][0-7][4-7]$ ]]; then
echo "WARN: .env file is world-readable (chmod 600 recommended)"
fi
Why This Architecture Works
The shared chokepoint pattern means security audits have one place to inspect rather than tracing through every tool handler. New tools added to Hermes automatically inherit the guard. For teams running in production, credentials stay compartmentalized per task, audit logs show exactly which agent accessed which key, and local processing never risks cloud credential exposure.
Audit trail example
[
{"provider":"openai","caller":"openai-completion-tool","purpose":"user-request","scope":"task","result":"GRANTED","timestamp":1723900800123},
{"provider":"anthropic","caller":"openai-completion-tool","purpose":"user-request","scope":"task","result":"DENIED","timestamp":1723900800124},
{"provider":"openai","caller":"background-summarizer","purpose":"background","scope":"session","result":"GRANTED","timestamp":1723900800125}
]
This log answers: Who accessed what, when, and was it authorized?
Operator verification: prove the guard works
Run these commands to verify the credential guard is active and effective on your installation:
# 1. Verify credentials are NOT in process.env during agent run
HERMES_DEBUG=1 hermes run "echo test" 2>&1 | grep -E '(OPENAI|ANTHROPIC|GOOGLE)_API_KEY'
# Should return NOTHING (keys never hit process.env)
# 2. Check audit log exists and records denials
cat ~/.hermes/logs/credential-audit.jsonl | jq 'select(.result=="DENIED")'
# Should show cross-provider access attempts being blocked
# 3. Verify .env is blocked from managed-files API
hermes file read .env
# Should error: "Path blocked by security policy"
# 4. Test case-insensitive scanner
echo "AnThRoPiC_ApI_KeY=sk-test" > .env.test
hermes config scan .env.test
# Should flag: "HIGH: Anthropic_API_Key detected (case-insensitive match)"
Related hardening in this cycle
These credential protections work alongside other Hermes security fixes:
| Fix | Article | Threat Mitigated |
|---|---|---|
| Secret redaction in worktrees | Secret Leakage + Windows Failures | Log leakage |
Case-insensitive .env guard |
Env File Guard | Config leakage |
| Cron job secret scope isolation | Cron Job Secret Scope | Scheduled task bleed |
| Browser private-page guard | Browser Guard | DOM credential theft |
All map directly onto Section 3 (Secrets & Environment) of the Coding Agent Security Checklist 2026.
Related articles
- Claude Code Alternatives in 2026: 12 Options Compared
- Context Engineering for Coding Agents: How to Make Every Token Count
- The AI Coding Trust Paradox: 42% of Code Is AI-Generated, But Only 29% of Developers Believe It
- Two Hermes Bugs Worth Watching: Secret Leakage in Redaction and Silent Windows Failures
- Coding Agent Security Checklist 2026 — The Operator’s Hardening Guide
Tired of deciding which AI subscription to keep? aiFiesta bundles GPT, Claude, Gemini, Grok, DeepSeek, Perplexity and more for $12/mo — less than half of a single premium chat sub. Tired of deciding which AI subscription to keep? aiFiesta bundles GPT, Claude, Gemini, Grok, DeepSeek, Perplexity and more for $12/mo — less than half of a single premium chat sub.