Skip to content

Security: mlennie/hermes-mcp

SECURITY.md

Security Policy

Reporting a vulnerability

Please do not open a public GitHub issue for security problems.

Use GitHub's private vulnerability reporting instead: go to the Security tab of this repository and click "Report a vulnerability". This opens a private advisory thread visible only to you and the maintainers.

Please include:

  • A description of the issue.
  • Steps to reproduce.
  • The version of hermes-mcp affected.
  • Your contact info if you'd like credit in the changelog.

You should expect an acknowledgement within 7 days. Please give us 90 days to ship a fix before public disclosure.

Supported versions

Security fixes land on the latest minor release. There is no LTS branch.

Threat model

For the full threat model — including adversary scenarios, design rationale, and residual risks — see THREAT_MODEL.md. The summary below is a quick reference.

hermes-mcp is a bridge: MCP client (Claude Desktop, Claude.ai, Codex desktop, Cursor, ...) → cloudflared/ngrok tunnel → hermes-mcp on 127.0.0.1:8765 → HTTP /v1/chat/completionshermes-gateway on 127.0.0.1:8642 → AIAgent loop. It supports two auth paths in parallel:

  • OAuth 2.1, PKCE-only (public client). PKCE code_verifier is the dynamic per-exchange secret. OAUTH_CLIENT_SECRET is accepted at /token for backward compatibility but not enforced — Claude's connector UI requires a value to send, but the server ignores it. Knowing OAUTH_CLIENT_ID + completing PKCE is what authenticates.
  • Static bearer token (MCP_BEARER_TOKEN, opt-in). Long-lived shared secret sent as Authorization: Bearer <token> on every /mcp request. Used by clients with no OAuth UI (Codex desktop's custom-MCP form, Cursor's headers block).

The bridge holds two genuinely-sensitive long-lived credentials: MCP_BEARER_TOKEN (if configured) and HERMES_API_KEY. Compromise of either is equivalent to remote action execution on the host at the privileges of the user running the gateway. OAUTH_CLIENT_SECRET is not in that category despite its name — see above.

Trust boundaries

Component Trust Notes
Host OS / shell Trusted If this is compromised, all bets are off.
hermes-mcp server Trusted Code under this repo.
hermes-gateway server Trusted Separate process owned by the same user. The bridge has no sandbox around it.
Tunnel edge (cloudflared / ngrok) Trusted transport TLS termination at the edge; we trust them not to MITM.
OAUTH_CLIENT_SECRET Not a security gate (despite name) Accepted at /token for backward compatibility (Claude UI requires a value to send) but server-side enforcement was removed when we switched to public-client OAuth. PKCE replaces it. Treat as a username, not a password.
MCP_BEARER_TOKEN (if set) Sensitive credential Long-lived static bearer accepted at /mcp. Leak = full compromise on that path. Generated by hermes-mcp mint-bearer-token; min 32 chars enforced.
HERMES_API_KEY Sensitive credential Bearer to the gateway. Never leaves the host.
MCP client (Claude / Codex / Cursor / ...) Authenticated Holds either OAuth credentials + minted access tokens, or the static bearer token.
Prompts arriving at hermes_ask Untrusted input May be poisoned by injection upstream.

Top risks

  1. MCP_BEARER_TOKEN leak (if configured). Anyone with the bearer + the tunnel URL can call /mcp directly — no further interaction required. Mitigations: hermes-mcp mint-bearer-token produces a ≥40-char secrets.token_urlsafe value; configuration enforces ≥32 characters; hmac.compare_digest at /mcp eliminates timing extraction; first-use audit log surfaces a single INFO line per process so anomalous use is visible in journal. Rotate (hermes-mcp mint-bearer-token, edit env, systemctl --user restart hermes-mcp) if exposed. If your only client is Claude (OAuth path), leave MCP_BEARER_TOKEN unset — opting in adds a long-lived credential you don't need.

  2. Gateway API-key leak. HERMES_API_KEY lets anyone on the host (or its loopback namespace) bypass the bridge entirely and call /v1/chat/completions directly. Mitigations: 0600 permissions on ~/.config/hermes-mcp/env. Run hermes-mcp and hermes-gateway as a dedicated low-privilege user with no other co-tenants.

  3. Prompt injection via the MCP client's context. A webpage or pasted file in a chat (Claude, Codex, Cursor, ...) tells the LLM to call hermes_ask with malicious instructions. Mitigations are mostly upstream and user-side:

    • Keep Hermes's approval hooks on. Do not run with --yolo.
    • Configure platform_toolsets.api_server in your Hermes config to a narrowly scoped toolset.
    • This bridge cannot reliably detect injection. The user controls Hermes's authorization model.
  4. Authorization-code interception (OAuth path). Mitigated by mandatory PKCE-S256: every /token exchange must present a code_verifier whose SHA-256 matches the code_challenge sent at /authorize — an attacker who captures the code (via a logged URL, a malicious redirect target, etc.) cannot redeem it without the verifier, which never leaves the legitimate client. Codes are single-use (atomic pop on exchange), expire in 60 seconds, and _StaticClient.validate_redirect_uri enforces a scheme allowlist (https and http-on-localhost always; plus operator-configured custom schemes via OAUTH_ALLOWED_REDIRECT_SCHEMES, default claude,claudeai,cursor) to prevent /authorize becoming an open redirector to javascript: / data: URIs. Operators extending the allowlist are responsible for picking schemes that aren't themselves dangerous.

  5. Refresh-token replay. Mitigated by atomic-pop-then-mint rotation: a second concurrent /token request with the same refresh token finds it gone and is rejected. This also approximates RFC 6819 reuse detection.

  6. DoS via unbounded state growth. /authorize is a public endpoint. Mitigated by MAX_OUTSTANDING_AUTH_CODES and MAX_OUTSTANDING_ACCESS_TOKENS caps with opportunistic reaping of expired entries.

  7. Information disclosure via logs. Prompt bodies and gateway response bodies are logged only at DEBUG. The default INFO level logs only endpoint, prompt_chars, session presence, and timeouts. Token-mint events log only the TTL. The OAuth state parameter is sanitized (newlines escaped, truncated) before logging to prevent log injection. Tunnel access logs may record IP and request volume; they do not see request bodies because TLS terminates there before the body is forwarded to the local server.

Out of scope for the threat model

  • Compromise of the host operating system.
  • Compromise of the cloudflared / ngrok account or their infrastructure.
  • Compromise of the user's MCP client account (Claude, OpenAI, Cursor, etc.) — would let an attacker into the same chats anyway.
  • Compromise of the hermes-gateway process itself (Scenario E in THREAT_MODEL.md).

No telemetry

hermes-mcp makes no outbound network requests other than what your tunnel software does and what Hermes itself does. No analytics, no error reporting, no version-check pings. If we ever add anything optional, it will be off by default and called out loudly here.

There aren't any published security advisories