New POST /mcp endpoint (Streamable HTTP transport, stateless — fresh
McpServer+transport per request) exposing one tool, ask_claude_code: runs
the real `claude` binary against a prompt, billed against the Pro/Max
subscription rather than API credits. This works specifically because it's
the actual claude CLI making the request server-side — the same reason
Hermes itself can't authenticate with the subscription directly (proven
earlier: Anthropic rejects the OAuth token from any client that isn't the
real CLI's exact request fingerprint). Read-only: no Edit/Write/git-push/
git-commit tools, since this is a quick-answer bridge, not a repo editor.
Tested end-to-end locally (built + ran the image, curled the full MCP
handshake: initialize -> tools/list -> tools/call) before pushing — got a
real 'pong' back from the actual claude CLI through the MCP protocol.
To register it with Hermes (lives in its own data volume, not git — see
.env.example comment):
docker exec hermes hermes config set mcp_servers.claude-code.url http://claude-agent:3001/mcp
docker exec hermes hermes config set 'mcp_servers.claude-code.headers.Authorization' 'Bearer <MCP_BRIDGE_KEY>'
By request: one auth layer, not two. Note this isn't really removing a
layer I added on top of nothing — Hermes's own gate is mandatory and
can't be disabled while the dashboard is reachable through a separate
Traefik container (it fails closed at startup on any non-loopback bind
without a configured auth provider). The only thing actually optional
was the Traefik-level middleware, so that's what comes out; Traefik now
just does TLS termination + routing.
Enabled via HERMES_DASHBOARD=1 (supervised in-container alongside the
gateway, per docs/user-guide/docker.md), bound to 0.0.0.0:9119 so Traefik
can reach it. Two independent auth layers, not one:
1. Traefik basicauth middleware in front of the whole route.
2. Hermes's own basic-auth gate (mandatory once the bind is non-loopback).
Hermes's docs explicitly call basic-auth-alone "not suitable for direct
public-internet exposure" and cite a real June 2026 incident where
internet scanners reached exposed dashboards and drove agents into
planting SSH-key backdoors — hence the extra Traefik-level gate rather
than relying on Hermes's own login page alone.
Also fixes: the htpasswd hash for Traefik's basicauth needs its literal
'$' characters escaped as '2824147' in .env, or docker compose's own variable
interpolation corrupts it (mistook '' for further
references). Also switched the hash from Python's default SHA-512
crypt ('$...') to apr1 ('$...', via openssl passwd -apr1) —
Traefik's basicauth middleware doesn't accept SHA-512-crypt.
Also re-adds Hermes's OPENAI_BASE_URL/OPENAI_API_KEY routing through the
local litellm gateway (instead of OPENROUTER_API_KEY direct) — this was
part of the now-abandoned PR #12 and never actually landed on main.
By request: one agent in Matrix, not several. Removes matrixBot.js,
router.js (chat-vs-code-task classifier), litellm.js (claude-agent's own
LiteLLM client), the matrix-bot-sdk dependency, runChatTask() and its
gitea.js branch/PR helpers (createBranch/createPullRequest — only ever
called from the now-removed chat flow), and every Matrix/LiteLLM env var
from claude-agent's compose service.
claude-agent already left the control room manually before this merge.
It keeps its Gitea-webhook-triggered PR review, which never touched Matrix
or LiteLLM to begin with.
Makes PR #12 (the claude-bot/Hermes cross-reply cascade fix) moot — the
bug can't happen once claude-agent has no Matrix client at all. Close#12
without merging once this lands.
Deployed as its own service (pinned nousresearch/hermes-agent:v2026.8.19),
own Matrix bot account (@hermes), own OpenRouter-backed model config, and
its own OpenAI-compatible API server (internal network only, for possible
future use by claude-agent). Joins the same control room but only responds
when explicitly @mentioned, restricted to the human user — no conflict with
claude-bot's default no-prefix chat routing. claude-agent's router now
ignores messages addressed to @hermes so both bots don't answer the same
message.
Bridge networking (the 'web' network), not the image's default host mode —
no reason for an agent container to share the host's network namespace when
everything it needs (the homeserver, OpenRouter) is reachable over the
existing bridge.
- New litellm service (pinned v1.98.0 — litellm 1.82.7/1.82.8 on PyPI were
compromised with credential-stealing malware in March 2026; internal-only,
no Traefik route, no reason to expose an LLM gateway with a master key
publicly).
- Replaces !claude/!ai command prefixes with automatic routing: every plain
message in the control room goes through a classifier (router.js) that
decides chat vs code_task. Chat replies use OpenRouter's own auto-router
(openrouter/auto) via LiteLLM; code_task requests go through the existing
runChatTask() flow (Claude Code CLI, unchanged, still using the
subscription token directly).
- Investigated routing Claude itself through LiteLLM via OAuth token
forwarding (general_settings.forward_client_headers_to_llm_api) so the
Pro/Max subscription could be one of the auto-routable options. Confirmed
non-functional: Anthropic returns a generic rate_limit_error for any
direct API call using this token type outside the real Claude Code CLI,
reproduced with plain curl straight to api.anthropic.com. Not included.
- MATRIX_BOT_USER_ID now required and set explicitly (self-message filtering
can no longer rely on a command-prefix mismatch once there isn't one).
Fixes a circular dependency: a full stack redeploy tears every service down
before bringing any back up, but claude-agent's image pull goes through
Traefik->Gitea's registry. Self-hosting Traefik in the same stack it fronts
means redeploying this stack could tear down the very route needed to bring
it back. Traefik now lives in ~/traefik/docker-compose.yml as shared infra,
independent of this stack's lifecycle.