Skip to main content
Meerkat’s auth story has two layers. The fast path is an env var (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY) — set it and rkat run works. The powerful path is realm-scoped bindings: declare a realm in your config, register credentials via rkat auth login or the REST/RPC surfaces, and pass --connection-ref <realm>:<binding> to scope a session or mob member to that specific binding.
Today, CLI rkat auth login is primarily a convenience bootstrap flow for the default dev:* credential namespace. If you need explicit realm/profile-targeted auth persistence such as prod:openai, use the REST/RPC auth surfaces or manage the realm config entries directly.
Both paths route through the same ProviderRuntimeRegistry. There’s no hidden magic path: the env fallback is a synthesized realm with CredentialSourceSpec::Env, resolved by the same code that handles your configured realms.

The fast path: env vars

export ANTHROPIC_API_KEY=sk-ant-...
rkat run "What's the weather like in 2026?"
That’s it. If the env var is set, Meerkat synthesizes a default realm (env_default) with a binding that reads the key via CredentialSourceSpec::Env, then resolves normally. RKAT_*-prefixed env vars (e.g., RKAT_ANTHROPIC_API_KEY) take precedence over the provider-native names.
ProviderPrimary env varFallback
AnthropicANTHROPIC_API_KEY
OpenAIOPENAI_API_KEY
GeminiGEMINI_API_KEYGOOGLE_API_KEY

The powerful path: realms + bindings

A realm is a named collection of backend profiles, auth profiles, and provider bindings. It lets you:
  • Run multiple sessions against different accounts or tenants
  • Mix OAuth (Claude.ai Pro/Max), api_key (keyed by CI), and cloud IAM (Bedrock/Vertex/Foundry) in one process
  • Hot-swap a session’s active binding mid-conversation
  • Give different mob members different credentials

Realm shape

# ~/.rkat/config.toml (or project-local .rkat/config.toml)

[realm.prod.backend.anthropic_primary]
provider = "anthropic"
backend_kind = "anthropic_api"

[realm.prod.backend.openai_primary]
provider = "openai"
backend_kind = "openai_api"

[realm.prod.auth.anthropic_oauth]
provider = "anthropic"
auth_method = "claude_ai_oauth"
source = { kind = "managed_store", profile = "anthropic_oauth" }

[realm.prod.auth.openai_apikey]
provider = "openai"
auth_method = "api_key"
source = { kind = "managed_store", profile = "openai_apikey" }

[realm.prod.binding.default]
backend_profile = "anthropic_primary"
auth_profile    = "anthropic_oauth"

[realm.prod.binding.openai]
backend_profile = "openai_primary"
auth_profile    = "openai_apikey"

Log in

# Interactive OAuth (opens a browser; stores refresh token in the
# TokenStore — OS keychain when available, otherwise 0600 file).
rkat auth login anthropic

# Scripted (api_key):
rkat auth login openai --non-interactive --secret "$OPENAI_KEY"
The current CLI login flow is convenient for local setup, but it does not yet let you choose an arbitrary target realm/profile name at login time. It seeds the runtime-managed default dev:* identities and is best thought of as the easiest way to get persisted credentials into Meerkat quickly.

Use the binding

rkat run --connection-ref prod:default "hello"
rkat run --connection-ref prod:openai   "hello"

Subcommands

1

Login and profile management

rkat auth realms                       # list realms defined in config
rkat auth profiles --realm prod        # list profiles/backends/bindings
rkat auth profile --realm prod <id>    # inspect one profile
rkat auth login <provider>             # start OAuth or api_key flow
rkat auth logout <profile_id>          # clear persisted credentials
2

Status and refresh

rkat auth status --realm prod <profile_id>    # TokenStore state + expiry
rkat auth test --realm prod <binding_id>      # dry-run the binding
rkat auth refresh --realm prod <profile_id>   # force-refresh OAuth tokens
rkat auth refresh is a no-op for api_key / static_bearer auth methods (nothing to refresh). For OAuth-backed methods it exchanges the persisted refresh token for a fresh access token and writes the new bundle back to the TokenStore.
3

Delete

rkat auth profile-delete --realm prod <profile_id> -y

Auth methods

Each provider’s runtime accepts a matrix of (backend_kind, auth_method) pairs. The table below summarizes what’s wired today.
Providerbackend_kindauth_methodNotes
Anthropicanthropic_apiapi_key, static_bearerDirect to api.anthropic.com
Anthropicanthropic_apiclaude_ai_oauthClaude Pro/Max OAuth (PKCE, S256)
Anthropicanthropic_apioauth_to_api_keyConsole OAuth → provisions api_key
Anthropicanthropic_apiexternal_authorizerHost-resolved auth injection
Anthropicbedrockbedrock_bearer, bedrock_aws_sigv4AWS SigV4 / env-bearer
Anthropicvertexvertex_google_authGoogleAuth / Vertex AI
Anthropicfoundryfoundry_api_key, foundry_azure_adAzure AI Foundry
OpenAIopenai_apiapi_key, static_bearer, external_authorizerDirect to api.openai.com or host-resolved auth
OpenAIchatgpt_backendmanaged_chatgpt_oauthChatGPT Plus/Pro OAuth
OpenAIchatgpt_backendexternal_chatgpt_tokensHost-supplied tokens
Googlegoogle_genaiapi_key, bearer_api_key, external_authorizerDirect to Google GenAI or host-resolved auth
Googlevertex_aiapi_key_expressVertex API Express key
Googlevertex_aiadc, compute_adcADC / compute metadata
Googlegoogle_code_assistgoogle_oauthGemini Code Assist OAuth
External commands (CredentialSourceSpec::Command) and file descriptors (CredentialSourceSpec::FileDescriptor) let host applications inject tokens without going through OAuth.

Credential sources

The source field on an auth profile declares where credentials come from.
KindDescription
InlineSecret { secret }Secret stored inline in the TOML (use only for dev/local)
Env { env, fallback }Read env var at resolve time via ResolverEnvironment::env_lookup
ManagedStore { profile }Resolve via the TokenStore; in current provider runtimes, persisted lookup is tied to the active auth-profile identity/binding path
PlatformDefaultUse the host platform’s default credential chain where supported
ExternalResolver { handle }Host-registered JS callback (WASM) or Rust resolver (embedded)
Command { program, args, ... }Run an external binary; cache stdout for refresh_interval_ms
FileDescriptor { fd, scope_override }Read credentials from a host-supplied file descriptor
Storing api keys inline in config files works but shouldn’t be checked into git. Prefer ManagedStore (which writes to the OS keychain or a 0o600 file) or Env.
FileDescriptor is a host-integration seam rather than a plain standalone resolver path. It requires a host-scoped reader/injection path, so treat it as an advanced embedding surface rather than a generic CLI config trick.

Hot-swap mid-session

When a session is created with --connection-ref prod:default, its persisted SessionMetadata.connection_ref carries that binding through resume and hot-swap. Changing the model/provider mid-turn via the RPC turn/start override keeps the binding by default; pass an explicit connection_ref on the turn request to scope the swap to a different realm or binding.
// RPC turn/start — hot-swap to openai binding in the same realm
{
  "method": "turn/start",
  "params": {
    "session_id": "...",
    "prompt": "...",
    "model": "gpt-5.4",
    "connection_ref": {"realm_id": "prod", "binding_id": "openai"}
  }
}
The factory re-enters ProviderRuntimeRegistry::resolve with the new binding — no env-default fallback, no cross-realm credential bleed.

Per-mob-member override

Mob members default to env-default / config-realm fallback credentials. Pass connection_ref on the host-side mob spawn surface to scope a member to a specific binding:
{
  "method": "mob/spawn",
  "params": {
    "mob_id": "research",
    "profile": "analyst",
    "agent_identity": "a1",
    "connection_ref": "prod:openai"
  }
}
Members in the same mob can use different providers or tenants without cross-contamination. The connection_ref string form (realm:binding) and struct form ({"realm_id": ..., "binding_id": ...}) are both accepted.

Refresh coordination

Refresh semantics are owned by the AuthMachine DSL (one instance per <realm>:<binding>). On resolve, the runtime:
  1. Checks expires_at. If present and within 80% of TTL, marks the lease Expiring via the DSL.
  2. Under Expiring, the next resolve triggers BeginRefresh. In-process dedup ensures only one refresh HTTP call per binding even under concurrent resolves; cross-process dedup via filesystem lockfile when refresh-file-lock is enabled.
  3. On successful refresh, fires CompleteRefresh and updates expires_at. On failure, RefreshFailedTransient (retry) or RefreshFailedPermanent (routes to ReauthRequired + emits an [AUTH_REAUTH_REQUIRED] system notice on the session).
The lifecycle is TLC-verified in specs/machines/auth/.

Audit logging

Every accepted lifecycle transition emits a structured tracing event:
  • target = "meerkat::auth::audit"
  • Fields: binding_key, action, from_phase, to_phase
REST and RPC surfaces also emit audit events for user-initiated actions: create_profile, delete_profile, login_oauth_complete, login_device_complete, logout. Filter any tracing::Subscriber on the meerkat::auth::audit target to build a persistent audit log.

Feature flags

The meerkat-providers crate gates optional auth methods behind features so you only pay for what you use:
FeatureEnables
oauthOAuth flows (Claude.ai, ChatGPT, Code Assist)
native-keyringOS keychain storage for tokens
refresh-file-lockCross-process refresh dedup
aws-authBedrock backend + SigV4 signing
google-oauthVertex + Code Assist + ADC
azure-authFoundry backend + Azure AD
The rkat CLI enables anthropic, openai, gemini, and oauth by default; add cloud-specific features at compile time for Bedrock/Vertex/Foundry.

See also

  • Hooks — observe auth events or gate turns on credential state
  • Mobs — per-member connection_ref overrides
  • Design doc /architecture/meerkat-runtime-dogma for the dogma §1/§10/§12 rationale behind the realm/binding model