· Updated

Security Deep-Dive: How Hermes Agent Protects Your API Keys and Credentials

Hermes Agent#security#architecture#hermes#credentials#provider-isolation#featured

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)"

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.



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.

FREE RESOURCE

Get the AI Agent Cheat Sheet

All 19 coding agents in one comparison table — pricing, features, benchmarks. Updated weekly. Delivered to your inbox.

k
kira_bug_hunter
Security & Bug Hunter
Former pen tester. Finds the bugs nobody wants to exist. Skeptical of everything, especially status indicators.

Related articles