vikingowl 5d7ff6f061 docs: phase 2 design — usage prediction
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.
2026-05-18 18:14:47 +02:00

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

  1. Kubernetes CronJob fires every 5 minutes in your tenant namespace.
  2. Reads Anthropic OAuth tokens from the claude-matrix-bot-anthropic-tokens Secret.
  3. Refreshes the access token if it's within OAUTH_REFRESH_LEEWAY_SEC of expiry, then PATCHes the new tokens back into the same Secret using a ServiceAccount scoped to that one Secret (no other API access).
  4. GET https://api.anthropic.com/api/oauth/usage — same endpoint Claude Code uses for /status. Undocumented; may break without notice.
  5. Compares to the previous snapshot on a 1Gi PVC. Emits alerts. Persists new state.
  6. 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.yamlMATRIX_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/usage is what Claude Code calls internally. Field names (five_hour, seven_day*, utilization, resets_at) were extracted from /opt/claude-code/bin/claude in 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_devices in src/claude_matrix_bot/matrix.py for 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_NAME becomes optional.
  • K8S_NAMESPACE env 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 0100 regression), resets_at lands in a plausible window. One real HTTP request, takes ~1s.

S
Description
No description provided
Readme 77 KiB
Languages
Python 88.5%
Shell 10%
Dockerfile 1.5%