Forward-looking signal: extrapolate the weekly buckets' burn rate from a rolling history and alert when the projection lands before the natural reset. Captures the design decisions made today (weekly buckets only, linear regression over 6h, schema v2 with backward-compatible read, configurable via env). Not yet implemented.
claude-matrix-bot
A Matrix bot for Claude-related operational signals, delivered into an end-to-end encrypted Matrix room.
Features
reset_watcher — feature #1
Watches your Claude subscription reset times and weekly usage. Alerts on:
| Trigger | Detail |
|---|---|
| Reset time shifts mid-window | A resets_at value moved while the previous window had not yet expired. Catches Anthropic adjusting the anchor on an active window. Natural window-rollover is silent. |
| Weekly utilization crosses 50% | Fires once per weekly window, for seven_day, seven_day_opus, seven_day_sonnet. |
| Weekly utilization crosses 80% | Same dedup. Highest default threshold. |
Thresholds are configurable via WEEKLY_THRESHOLDS env ("50,80" default).
Planned
- Usage rate prediction — extrapolate weekly utilization curve and alert when projected to exhaust before reset. (Phase 2.)
How reset_watcher runs
- Kubernetes
CronJobfires every 5 minutes in your tenant namespace. - Reads Anthropic OAuth tokens from the
claude-matrix-bot-anthropic-tokensSecret. - Refreshes the access token if it's within
OAUTH_REFRESH_LEEWAY_SECof expiry, then PATCHes the new tokens back into the same Secret using aServiceAccountscoped to that one Secret (no other API access). GET https://api.anthropic.com/api/oauth/usage— same endpoint Claude Code uses for/status. Undocumented; may break without notice.- Compares to the previous snapshot on a 1Gi
PVC. Emits alerts. Persists new state. - On first run or when alerts fire, posts to your encrypted Matrix room via
matrix-nio[e2e]. The bot's Olm/Megolm key store lives on the same PVC, so the device identity is stable across runs. First run posts a "🤖 watcher armed" baseline so you know the bot is live.
Package layout
src/claude_matrix_bot/
__init__.py
config.py env-driven config, fails fast on missing values
matrix.py nio-based E2EE sender (TOFU device trust)
clients/
anthropic.py /api/oauth/usage + token refresh
k8s.py PATCH the OAuth Secret in-cluster
reset_watcher/
__main__.py CronJob entrypoint
diff.py pure alert/threshold logic
format.py Alert -> (plain, html) for Matrix
state.py atomic JSON load/save
tests/ pytest, no network
deploy/k8s/ manifests + kustomization
Entrypoint: python -m claude_matrix_bot.reset_watcher.
One-time setup
1. Build and push the image
The target registry is registry.itsh.dev (Zot), which requires OCI image
indexes wrapped with SLSA provenance attestations. A plain docker build
followed by docker push produces a bare manifest and gets rejected with
manifest invalid. Use the helper script:
./scripts/build-push.sh # builds the tag pinned in cronjob.yaml
TAG=0.2.0 ./scripts/build-push.sh # release a new version
The script auto-detects the current pinned tag from deploy/k8s/cronjob.yaml,
runs docker buildx build --provenance mode=max --platform linux/amd64 --push,
and verifies the manifest landed. When you bump versions, edit the image:
ref in cronjob.yaml after the push, then kubectl apply -k deploy/k8s.
2. Create the Matrix bot account
Use a dedicated user — not your own account. If the bot shares a Matrix user with your real Element session, every CronJob run pulls a full sync of every encrypted room you're in (slow, log-noisy), and you risk device-id collisions that produce "unable to decrypt" warnings until you sign sessions out manually.
Register a fresh user on your homeserver (e.g. @claude-matrix-bot:example.org)
and invite that bot into your alert room from your own session. Then get the
bot's credentials via a direct login (not Element — that ties the device to your
browser session):
curl -X POST https://matrix.example.org/_matrix/client/v3/login \
-H "Content-Type: application/json" \
-d '{"type":"m.login.password",
"identifier":{"type":"m.id.user","user":"claude-matrix-bot"},
"password":"<bot-password>",
"initial_device_display_name":"claude-matrix-bot"}'
The response gives you access_token and device_id. Use that pair forever —
re-logging-in mints a new device and breaks E2EE for past messages.
If you must reuse your own account during testing, make a second login pair
via the same curl rather than copying credentials out of Element. Reusing
Element's device_id in another client desyncs the keys and produces an
"unable to decrypt" loop on the recipient side.
3. Provision the Secrets
kubectl -n tenant-2 create secret generic claude-matrix-bot-anthropic-tokens \
--from-literal=accessToken="$(jq -r .claudeAiOauth.accessToken ~/.claude/.credentials.json)" \
--from-literal=refreshToken="$(jq -r .claudeAiOauth.refreshToken ~/.claude/.credentials.json)" \
--from-literal=expiresAt="$(jq -r .claudeAiOauth.expiresAt ~/.claude/.credentials.json)"
kubectl -n tenant-2 create secret generic claude-matrix-bot-matrix-creds \
--from-literal=homeserver_url="https://matrix.example.org" \
--from-literal=user_id="@claude-matrix-bot:example.org" \
--from-literal=access_token="syt_..." \
--from-literal=device_id="ABCDEFGHIJ"
4. Set the room ID
Edit deploy/k8s/configmap.yaml → MATRIX_ROOM_ID. Element shows it under
Room → Settings → Advanced.
5. Apply
kubectl -n tenant-2 apply -k deploy/k8s
Verify:
kubectl -n tenant-2 get cronjob,pvc,sa,role,rolebinding,configmap
kubectl -n tenant-2 create job --from=cronjob/claude-matrix-bot-reset-watcher manual-test-1
kubectl -n tenant-2 logs -f job/manual-test-1
The first run posts a 🤖 reset-watcher armed message with the baseline
snapshot. From there on, only alerts.
Trust caveats
- Undocumented endpoint.
/api/oauth/usageis what Claude Code calls internally. Field names (five_hour,seven_day*,utilization,resets_at) were extracted from/opt/claude-code/bin/claudein May 2026. Anthropic can change either at any time. If alerts go silent or 4xx appears in logs, this is the first place to look. - Trust-on-first-use Matrix devices. The bot accepts whatever keys the
homeserver reports for room members on each sync. Sufficient if you control
the homeserver. Swap
_trust_room_devicesinsrc/claude_matrix_bot/matrix.pyfor cross-signing if you want strict verification.
Local Docker test (LOCAL_MODE)
LOCAL_MODE=1 relaxes the K8s-specific behavior so you can drive the bot from plain
docker run against a bind-mounted oauth-tokens dir:
K8S_OAUTH_SECRET_NAMEbecomes optional.K8S_NAMESPACEenv var can replace the SA-projected namespace file (which doesn't exist outside a cluster).- On token refresh, the new tokens are atomically written back to the mounted
OAUTH_TOKENS_DIR(three files:accessToken/refreshToken/expiresAt) instead of patching a K8s Secret. Your host bind-mount needs to be writable.
One-time local setup
Use the bootstrap script — it slurps the Anthropic OAuth tokens out of your Claude install and prompts for the Matrix bot credentials.
./scripts/bootstrap-local.sh
If you keep your Claude config in a non-default directory (work/priv split,
multi-account setup, etc.) point the script at it via positional arg or
CLAUDE_CONFIG_DIR:
./scripts/bootstrap-local.sh ~/.claude-priv
CLAUDE_CONFIG_DIR=$HOME/.claude-work ./scripts/bootstrap-local.sh
The script is idempotent and remembers existing Matrix values across runs
(useful when you refresh tokens via a docker run and just need to re-bootstrap).
Build + run
docker build -t claude-matrix-bot:dev .
docker run --rm \
-e LOCAL_MODE=1 \
-e MATRIX_ROOM_ID='!yourroom:matrix.example.org' \
-e WEEKLY_THRESHOLDS=50,80 \
-e OAUTH_REFRESH_LEEWAY_SEC=600 \
-v "$PWD/local/oauth-tokens:/var/run/secrets/oauth-tokens" \
-v "$PWD/local/matrix-bot:/var/run/secrets/matrix-bot:ro" \
-v "$PWD/local/state:/state" \
claude-matrix-bot:dev
First run posts the 🤖 reset-watcher armed baseline to your Matrix room and writes
./local/state/state.json plus ./local/state/nio-store/. Subsequent runs only post
when something changes. To force a threshold alert, edit state.json to drop one of
the seven_day*.utilization values below 50 and clear its alerted_thresholds.
Development
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt pytest
PYTHONPATH=src python -m pytest tests/ -v
Live API smoke test
A live pytest marker exists for the case "Anthropic silently changed the
/api/oauth/usage response shape." It's skipped by default; CI never runs it.
Run it locally whenever you want a quick sanity check that the parse still
holds:
PYTHONPATH=src LIVE_TOKENS_DIR=$PWD/local/oauth-tokens \
python -m pytest tests/ -m live -v
Asserts: utilization stays fractional (catches the 0–100 regression),
resets_at lands in a plausible window. One real HTTP request, takes ~1s.