william 732246d4fd Add MCP bridge to claude-agent so Hermes can delegate to the real Claude Code CLI
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>'
2026-08-23 17:24:46 +00:00

gitops-automation

Claude Code + Hermes automation wired into Gitea + Portainer + Matrix on this VPS. See ~/.claude/plans/cozy-honking-lantern.md on the host for the original design rationale (some of it — the Matrix chat bot on claude-agent — has since been superseded, see below).

What this gives you today:

  • PR review: opening/updating a PR in a watched Gitea repo gets a Claude-authored review comment (claude-agent, triggered by a Gitea webhook — nothing to do with Matrix).
  • GitOps redeploy: pushing to main on this repo rebuilds the claude-agent image (Gitea Actions) and redeploys the stack, chained as the last step of that same workflow.
  • Matrix: Hermes is the only agent present in Matrix. claude-agent used to also run a Matrix bot (!claude/!ai commands, then no-prefix auto-routing) — that's been removed entirely (by request: one agent in Matrix, not several). Hermes has its own native Matrix connection, responds to @hermes <message> in shared rooms (no mention needed in DMs), and has its own tools (terminal, code execution, web search, etc.) — see its docs at https://hermes-agent.nousresearch.com for what it can do. It does not (yet) have the old branch/PR-opening workflow the Matrix bot used to have; that logic still exists in git history if it's worth reviving as a Hermes tool/skill later.

Nothing here auto-merges PRs. Every path stops at a comment or an open PR — a human clicks merge (enforced by branch protection on main, not just by convention — see below).

Prerequisites (one-time, on the VPS)

sudo usermod -aG docker william   # then start a new shell/session
docker network create web

Bring-up order

  1. DNS — confirm these resolve to 217.160.66.143: gitea.apps.williamturner.eu, portainer.apps.williamturner.eu, matrix.apps.williamturner.eu, agent.apps.williamturner.eu.

  2. Front Gitea with Traefik: edit ~/gitea/docker-compose.yml — add it to the web network and Traefik labels (mirror the matrix-homeserver block in this repo's docker-compose.yml, using port 3000 as the service port and rule Host(`gitea.apps.williamturner.eu`)). Update ~/gitea/data/gitea/conf/app.ini: DOMAIN and ROOT_URLhttps://gitea.apps.williamturner.eu/. Recreate the container, confirm the HTTPS URL works, then remove 3000:3000 from the port mapping.

  3. Front Portainer with Traefik: same pattern on ~/portainer-compose.yaml, service port 9443 (Portainer serves TLS itself on that port — either terminate TLS at Traefik with traefik.http.services.portainer.loadbalancer.server.scheme=https and serversTransport with insecure skip-verify, or simplest: also expose Portainer's plain HTTP port internally and point Traefik at that instead). Confirm https://portainer.apps.williamturner.eu works before removing 9443:9443.

  4. Create the gitops-automation repo in Gitea (via http://217.160.66.143:3000 if step 2 isn't done yet, otherwise the HTTPS URL), push this directory to it.

  5. Gitea API token: user Settings → Applications → generate a token with repo + webhook scopes. Put it in .env as GITEA_TOKEN.

  6. Gitea Actions runner: admin Settings → Actions → Runners → create registration token → .env as ACT_RUNNER_REGISTRATION_TOKEN. Also set repo-level Actions variables GITEA_HOST (e.g. gitea.apps.williamturner.eu) and secret GITEA_TOKEN (Settings → Actions → Variables/Secrets on the repo) — the workflow in .gitea/workflows/build.yml reads those.

  7. First image build (registry is empty until Actions runs once):

    cp .env.example .env   # fill in values as you go
    docker build -t "$(grep GITEA_REGISTRY_IMAGE .env | cut -d= -f2)" ./agent
    docker login <gitea-host> -u <your-username>
    docker push "$(grep GITEA_REGISTRY_IMAGE .env | cut -d= -f2)"
    
  8. Bring up the stack, ideally as a Portainer "Repository" stack pointed at this repo (so it's also the GitOps redeploy target) — or directly:

    docker compose up -d
    
  9. First boot: Matrix bot account — with MATRIX_ALLOW_REGISTRATION=true in .env, redeploy matrix-homeserver, then register the bot:

    curl -s https://matrix.apps.williamturner.eu/_matrix/client/v3/register \
      -H 'Content-Type: application/json' \
      -d '{"username":"claude-bot","password":"<pick one>","auth":{"type":"m.login.dummy"}}'
    

    This returns an access_token — put it in .env as MATRIX_BOT_TOKEN. Then set MATRIX_ALLOW_REGISTRATION=false and redeploy matrix-homeserver again.

  10. Control room: from any Matrix client logged in as yourself on this homeserver, create a room, invite @claude-bot:matrix.apps.williamturner.eu, copy the room ID into .env as MATRIX_CONTROL_ROOM_ID. Redeploy claude-agent.

  11. Portainer stack webhook: in the stack's settings, enable the webhook, copy the URL into .env/repo secrets as PORTAINER_STACK_WEBHOOK_URL.

  12. Gitea webhooks on each repo you want automation for:

    • push → mainPORTAINER_STACK_WEBHOOK_URL (only needed on this repo, for GitOps redeploy of the automation stack itself)
    • pull request (opened, synchronized) → https://agent.apps.williamturner.eu/webhooks/gitea, secret = GITEA_WEBHOOK_SECRET, on every repo you want auto-reviewed.
  13. Firewall: sudo ufw allow 80/tcp 443/tcp; once the HTTPS routes above are all confirmed working, sudo ufw delete allow 3000/tcp and sudo ufw delete allow 9443/tcp.

  14. Branch protection on main, on every repo you want the bot working on (so it's structurally limited to opening PRs, never merging or pushing directly) — via the repo's Settings → Branches → Add Rule, or the API:

    curl -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
      "$GITEA_URL/api/v1/repos/<owner>/<repo>/branch_protections" \
      -d '{
        "rule_name": "main",
        "enable_push": false,
        "enable_merge_whitelist": true,
        "merge_whitelist_usernames": ["<your-human-username>"]
      }'
    

    This blocks all direct pushes to main (everyone goes through a PR) and restricts merging to the human usernames listed — claude-bot (see below) is never in that list, so it structurally cannot merge, only open PRs, regardless of what its token can do.

  15. Dedicated bot account (recommended over using your own account/token for the agent): create a separate Gitea user (e.g. claude-bot) via Site Administration → User Accounts, generate its own token (scopes: repository, issue, package — no admin, organization, or user needed), add it as a repo Collaborator with Write permission on each automated repo, then use its token as GITEA_TOKEN (agent) and REGISTRY_TOKEN (CI secret) instead of your own. This makes every PR comment, branch, and PR clearly attributed to the bot instead of you, and its access is easy to revoke independently.

Smoke test

  • Open a throwaway PR on a repo with the PR webhook set → expect a Claude review comment.
  • In the Matrix control room: @hermes hello → expect a reply from Hermes.
  • git push to main on this repo (touching agent/**) → expect a Gitea Actions run, then a chained Portainer redeploy at the end of that same workflow.

Notes

  • agent/src/runner.js only ever runs read-only git clone/fetch/checkout for the PR diff it reviews — Claude Code itself is denied Edit/Write/commit/push tools (--disallowedTools), so this path can never modify a repo, only comment on it.
  • .gitea/workflows/build.yml assumes the act_runner label docker — check GITEA_RUNNER_LABELS in docker-compose.yml matches what you actually registered.
  • Hermes's own config/memory/skills live in /home/william/hermes-data on the host (bind-mounted, not in this repo) — back that up separately if it accumulates anything worth keeping.
S
Description
No description provided
Readme
206 KiB
Languages
JavaScript 91.9%
Dockerfile 8.1%