Skip to content

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 exposes claude_accounts (whose oauth_config JSONB holds plaintext access and refresh tokens), mcp_oauth_credentials, x_oauth_credentials, and runtime_login_attempts as editable resources.

app/controllers/supervisor/application_controller.rb is the whole story:

before_action :authenticate_supervisor
def authenticate_supervisor
# TODO Add authentication logic here.
end

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/elicitations and GET /api/v1/elicitations/:id — required by the MCP fallback-elicitation protocol, since the MCP child process has no key.
  • GET /api/secrets/keys — because Api::SecretsController inherits ApplicationController, not Api::BaseController. It leaks secret names and descriptions (not values), unauthenticated.

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.

Agent harness credentials

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.

MCP server OAuth

VarUsed for
API_KEYSREST API auth (comma-separated)
APP_HOSTThe MCP OAuth redirect URI. Defaults to localhost:3000, and picks http iff the host string contains “localhost”.
RAILS_MASTER_KEYUnlocks Rails credentials (mcp_oauth_clients, mcp_secrets)
X_OAUTH_CLIENT_ID / _SECRETX/Twitter token vending
ANTHROPIC_API_KEYLocal dev, when not using OAuth