Auth architecture
Zimmer has four separate authentication systems that share almost nothing. Understanding which is which is most of the battle.
1. Human → Zimmer: there is no authentication
Section titled “1. Human → Zimmer: there is no authentication”This is not a simplification. ApplicationController has no before_action for auth, no session
auth, no Devise, no OmniAuth, no HTTP Basic. There are no login routes. There is no User model in
the auth path.
Everything is open to anyone who can reach the host:
- the session dashboard and every transcript,
/settings,/quotas(including the OAuth login flow),- the GoodJob dashboard at
/jobs, - and
/supervisor— the Administrate admin panel, which exposesclaude_accounts(whoseoauth_configJSONB holds plaintext access and refresh tokens),mcp_oauth_credentials,x_oauth_credentials, andruntime_login_attemptsas editable resources.
app/controllers/supervisor/application_controller.rb is the whole story:
before_action :authenticate_supervisor
def authenticate_supervisor # TODO Add authentication logic here.end2. Client → REST API: X-API-Key
Section titled “2. Client → REST API: X-API-Key”The only authenticated surface. Api::BaseController#authenticate_api_key compares the X-API-Key
header against ENV["API_KEYS"] (comma-separated) using a constant-time comparison.
What it isn’t:
- No scoping. Keys are opaque strings with no identity, no permissions, no ownership. Any valid key can read, mutate, and delete every session, trigger, and category.
- No rotation without a restart — the valid-key list is memoized per request instance from ENV.
- No audit trail of which key did what.
Three endpoints skip it entirely:
POST /api/v1/elicitationsandGET /api/v1/elicitations/:id— required by the MCP fallback-elicitation protocol, since the MCP child process has no key.GET /api/secrets/keys— becauseApi::SecretsControllerinheritsApplicationController, notApi::BaseController. It leaks secret names and descriptions (not values), unauthenticated.
3. Zimmer → the agent vendor
Section titled “3. Zimmer → the agent vendor”A pool of accounts (ClaudeAccount — misleadingly named; it serves both runtimes, discriminated
by a runtime column) with automatic OAuth refresh and automatic rotation when one hits its quota.
4. The agent → MCP servers
Section titled “4. The agent → MCP servers”A completely separate system: McpOauthCredential + McpOauthPendingFlow, doing full RFC 8414
discovery, RFC 7591 dynamic client registration, and PKCE — then writing the resulting tokens into
the CLI’s own credential file so the agent’s MCP client picks them up.
Nothing is encrypted at rest
Section titled “Nothing is encrypted at rest”The environment variables that matter
Section titled “The environment variables that matter”| Var | Used for |
|---|---|
API_KEYS | REST API auth (comma-separated) |
APP_HOST | The MCP OAuth redirect URI. Defaults to localhost:3000, and picks http iff the host string contains “localhost”. |
RAILS_MASTER_KEY | Unlocks Rails credentials (mcp_oauth_clients, mcp_secrets) |
X_OAUTH_CLIENT_ID / _SECRET | X/Twitter token vending |
ANTHROPIC_API_KEY | Local dev, when not using OAuth |