You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every ct invocation runs the full login handshake against ChurchTools. A short
series of read-only calls therefore trips CT's login rate limit and every
subsequent command fails before it reaches its actual endpoint:
$ ct get raw "/persons/3033/groups?limit=200" --env prod
✗ Login failed (whoami) (HTTP 429)
{ "message": "Too many requests", ... }
Three ct get raw calls in a row were enough to trigger it, and it was still
tripped after a 45s pause. The 429 comes from the handshake, not from the
resource endpoint — the request never gets that far.
Why
CtApiClient holds the session cookie and CSRF token in instance fields
(src/api/ctClient.ts:120-145, authenticate() → captureCookie + refreshCsrfToken). The CLI is one-shot, so the instance dies with the process
and the next invocation redoes GET /api/whoami?login_token=… + GET /api/csrftoken. What is persisted per host is the login token (Keychain),
which is a credential, not a session — so nothing carries the handshake across
processes.
This is invisible for the primary workflow (plan / apply — one process, one
login) and only bites on the read path, where using the CLI as intended means
many short invocations: ct get, ct get raw, ct adopt, ct state, scripted
loops.
Suggested fix
Cache the session cookie + CSRF token on disk next to the stored credential,
keyed by host, and reuse it when still valid:
On authenticate(), write { cookie, csrfToken, obtainedAt } for that host
(mode 0600, same store/dir as the existing per-host credential — the cookie is
as sensitive as the token, so it must not land in a world-readable file or in
the repo).
On startup, load the cached session and skip the handshake; on the first 401
from a real request, drop the cache, re-authenticate once, and retry — so an
expired or server-invalidated session self-heals rather than erroring out.
ct auth logout must delete the cached session along with the token.
Two smaller things worth deciding alongside it:
Backoff on 429 from the handshake.fetchWithRetry treats the whoami call
as idempotent; if it does not already honour Retry-After for 429, doing so
would turn a hard failure into a wait.
A clearer error.✗ Login failed (whoami) (HTTP 429) reads as "your
credentials are wrong". For a 429 specifically it should say the login rate
limit was hit and roughly how long to wait.
Repro
foridin 3033 92 27813;do ct get raw "/persons/$id/groups?limit=200" --env prod;done
Observed on ct 0.1.0 (local build), host eqrm.church.tools, 2026-08-24.
What happens
Every
ctinvocation runs the full login handshake against ChurchTools. A shortseries of read-only calls therefore trips CT's login rate limit and every
subsequent command fails before it reaches its actual endpoint:
Three
ct get rawcalls in a row were enough to trigger it, and it was stilltripped after a 45s pause. The 429 comes from the handshake, not from the
resource endpoint — the request never gets that far.
Why
CtApiClientholds the session cookie and CSRF token in instance fields(
src/api/ctClient.ts:120-145,authenticate()→captureCookie+refreshCsrfToken). The CLI is one-shot, so the instance dies with the processand the next invocation redoes
GET /api/whoami?login_token=…+GET /api/csrftoken. What is persisted per host is the login token (Keychain),which is a credential, not a session — so nothing carries the handshake across
processes.
This is invisible for the primary workflow (
plan/apply— one process, onelogin) and only bites on the read path, where using the CLI as intended means
many short invocations:
ct get,ct get raw,ct adopt,ct state, scriptedloops.
Suggested fix
Cache the session cookie + CSRF token on disk next to the stored credential,
keyed by host, and reuse it when still valid:
authenticate(), write{ cookie, csrfToken, obtainedAt }for that host(mode 0600, same store/dir as the existing per-host credential — the cookie is
as sensitive as the token, so it must not land in a world-readable file or in
the repo).
401from a real request, drop the cache, re-authenticate once, and retry — so an
expired or server-invalidated session self-heals rather than erroring out.
session must never be sent to a host other than the one it was captured
against.
ct auth logoutmust delete the cached session along with the token.Two smaller things worth deciding alongside it:
fetchWithRetrytreats the whoami callas idempotent; if it does not already honour
Retry-Afterfor 429, doing sowould turn a hard failure into a wait.
✗ Login failed (whoami) (HTTP 429)reads as "yourcredentials are wrong". For a 429 specifically it should say the login rate
limit was hit and roughly how long to wait.
Repro
Observed on
ct0.1.0 (local build), hosteqrm.church.tools, 2026-08-24.