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-mcpaffected. - 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.
Security fixes land on the latest minor release. There is no LTS branch.
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/completions → hermes-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_verifieris the dynamic per-exchange secret.OAUTH_CLIENT_SECRETis accepted at/tokenfor backward compatibility but not enforced — Claude's connector UI requires a value to send, but the server ignores it. KnowingOAUTH_CLIENT_ID+ completing PKCE is what authenticates. - Static bearer token (
MCP_BEARER_TOKEN, opt-in). Long-lived shared secret sent asAuthorization: Bearer <token>on every/mcprequest. Used by clients with no OAuth UI (Codex desktop's custom-MCP form, Cursor'sheadersblock).
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.
| 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. |
-
MCP_BEARER_TOKENleak (if configured). Anyone with the bearer + the tunnel URL can call/mcpdirectly — no further interaction required. Mitigations:hermes-mcp mint-bearer-tokenproduces a ≥40-charsecrets.token_urlsafevalue; configuration enforces ≥32 characters;hmac.compare_digestat /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), leaveMCP_BEARER_TOKENunset — opting in adds a long-lived credential you don't need. -
Gateway API-key leak.
HERMES_API_KEYlets anyone on the host (or its loopback namespace) bypass the bridge entirely and call/v1/chat/completionsdirectly. Mitigations:0600permissions on~/.config/hermes-mcp/env. Runhermes-mcpandhermes-gatewayas a dedicated low-privilege user with no other co-tenants. -
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_askwith malicious instructions. Mitigations are mostly upstream and user-side:- Keep Hermes's approval hooks on. Do not run with
--yolo. - Configure
platform_toolsets.api_serverin your Hermes config to a narrowly scoped toolset. - This bridge cannot reliably detect injection. The user controls Hermes's authorization model.
- Keep Hermes's approval hooks on. Do not run with
-
Authorization-code interception (OAuth path). Mitigated by mandatory PKCE-S256: every
/tokenexchange must present acode_verifierwhose SHA-256 matches thecode_challengesent 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_urienforces a scheme allowlist (httpsandhttp-on-localhost always; plus operator-configured custom schemes viaOAUTH_ALLOWED_REDIRECT_SCHEMES, defaultclaude,claudeai,cursor) to prevent/authorizebecoming an open redirector tojavascript:/data:URIs. Operators extending the allowlist are responsible for picking schemes that aren't themselves dangerous. -
Refresh-token replay. Mitigated by atomic-pop-then-mint rotation: a second concurrent
/tokenrequest with the same refresh token finds it gone and is rejected. This also approximates RFC 6819 reuse detection. -
DoS via unbounded state growth.
/authorizeis a public endpoint. Mitigated byMAX_OUTSTANDING_AUTH_CODESandMAX_OUTSTANDING_ACCESS_TOKENScaps with opportunistic reaping of expired entries. -
Information disclosure via logs. Prompt bodies and gateway response bodies are logged only at
DEBUG. The defaultINFOlevel logs onlyendpoint,prompt_chars, session presence, and timeouts. Token-mint events log only the TTL. The OAuthstateparameter 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.
- 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-gatewayprocess itself (Scenario E in THREAT_MODEL.md).
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.