Not actually needed: self-message filtering was only used to avoid the bot reacting to its own replies, but those never match the !claude command pattern anyway, so parseCommand() already filters them out for free.
gitops-automation
Claude Code automation wired into Gitea + Portainer + Matrix on this VPS. See
~/.claude/plans/cozy-honking-lantern.md on the host for the full design rationale.
Three things this gives you:
- PR review: opening/updating a PR in a watched Gitea repo gets a Claude-authored review comment.
- GitOps redeploy: pushing to
mainon this repo rebuilds theclaude-agentimage (Gitea Actions) and redeploys the stack (Portainer webhook). - Chat-driven coding agent:
!claude owner/repo <instruction>in the Matrix control room clones the repo, runs Claude Code, and opens a PR with the result.
Nothing here auto-merges. Every path stops at a comment or an open PR — a human clicks merge.
Prerequisites (one-time, on the VPS)
sudo usermod -aG docker william # then start a new shell/session
docker network create web
Bring-up order
-
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. -
Front Gitea with Traefik: edit
~/gitea/docker-compose.yml— add it to thewebnetwork and Traefik labels (mirror thematrix-homeserverblock in this repo'sdocker-compose.yml, using port3000as the service port and ruleHost(`gitea.apps.williamturner.eu`)). Update~/gitea/data/gitea/conf/app.ini:DOMAINandROOT_URL→https://gitea.apps.williamturner.eu/. Recreate the container, confirm the HTTPS URL works, then remove3000:3000from the port mapping. -
Front Portainer with Traefik: same pattern on
~/portainer-compose.yaml, service port9443(Portainer serves TLS itself on that port — either terminate TLS at Traefik withtraefik.http.services.portainer.loadbalancer.server.scheme=httpsandserversTransportwith insecure skip-verify, or simplest: also expose Portainer's plain HTTP port internally and point Traefik at that instead). Confirmhttps://portainer.apps.williamturner.euworks before removing9443:9443. -
Create the
gitops-automationrepo in Gitea (viahttp://217.160.66.143:3000if step 2 isn't done yet, otherwise the HTTPS URL), push this directory to it. -
Gitea API token: user Settings → Applications → generate a token with repo + webhook scopes. Put it in
.envasGITEA_TOKEN. -
Gitea Actions runner: admin Settings → Actions → Runners → create registration token →
.envasACT_RUNNER_REGISTRATION_TOKEN. Also set repo-level Actions variablesGITEA_HOST(e.g.gitea.apps.williamturner.eu) and secretGITEA_TOKEN(Settings → Actions → Variables/Secrets on the repo) — the workflow in.gitea/workflows/build.ymlreads those. -
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)" -
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 -
First boot: Matrix bot account — with
MATRIX_ALLOW_REGISTRATION=truein.env, redeploymatrix-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.envasMATRIX_BOT_TOKEN. Then setMATRIX_ALLOW_REGISTRATION=falseand redeploymatrix-homeserveragain. -
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.envasMATRIX_CONTROL_ROOM_ID. Redeployclaude-agent. -
Portainer stack webhook: in the stack's settings, enable the webhook, copy the URL into
.env/repo secrets asPORTAINER_STACK_WEBHOOK_URL. -
Gitea webhooks on each repo you want automation for:
- push →
main→PORTAINER_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.
- push →
-
Firewall:
sudo ufw allow 80/tcp 443/tcp; once the HTTPS routes above are all confirmed working,sudo ufw delete allow 3000/tcpandsudo ufw delete allow 9443/tcp.
Smoke test
- Open a throwaway PR on a repo with the PR webhook set → expect a Claude review comment.
- In the Matrix control room:
!claude owner/repo add a comment to the README→ expect a "working on it" reply, then a PR link. git pushtomainon this repo → expect a Gitea Actions run, then a Portainer redeploy.
Notes
agent/src/runner.jsis the only thing that ever runsgit commit/git push/git checkout— Claude Code itself is explicitly denied those tools (--disallowedTools), so even a misbehaving prompt can't push directly or touchmain.- The Matrix bot only reacts inside
MATRIX_CONTROL_ROOM_ID; keep that room invite-only. .gitea/workflows/build.ymlassumes the act_runner labeldocker— checkGITEA_RUNNER_LABELSindocker-compose.ymlmatches what you actually registered.