Skip to content

Configuration reference

FileWhat it isRead by
air.jsonThe AIR catalog wiring (dev/test)AirCatalogService via the AIR CLI
air.production.jsonSame, for the in-image catalog (prod/staging)ditto
roots.jsonAgent root definitionsAgentRootsConfig
mcp.jsonMCP server registryServersConfig
skills/skills.jsonSkill indexSkillsConfig
plugins/plugins.jsonPlugin indexPluginsConfig
hooks/hooks.jsonHook indexHooksConfig
references/references.jsonReference indexReferencesConfig
config/goals.jsonGoal / stop-condition catalogGoalsConfig

config/goals.json is the exception — it’s a plain static file that GoalsConfig reads directly, outside AIR.

VarPurposeSet by the shipped deploy?
SECRET_KEY_BASERails secret✅ (Kamal)
DATABASE_HOST / _PORT / _USERNAME / _PASSWORD / _SSLMODEPostgres✅ (Kamal)
REDIS_URLCache✅ (Kamal)
API_KEYSREST API auth✅ (Kamal)
SUPERVISOR_PASSWORDThe HTTP Basic realm in front of /supervisor. Fails closed — unset or blank means every Administrate dashboard returns 401, including for you. Optional SUPERVISOR_USERNAME defaults to supervisor❌ — seed it into your deploy secrets, then add it to env.secret in config/deploy.*.yml
APP_HOSTMCP OAuth redirect URI, and the mailer link host✅ (Kamal)
ZIMMER_PROD_BASE_URL / ZIMMER_STAGING_BASE_URLExternally-reachable base URL of this instance (e.g. https://zimmer.your-domain.com). AppUrl resolves it to build every absolute link Zimmer emits — session URLs in the orchestrator system prompt, “View trigger in Zimmer” alert links, MCP tool output. Set this: when unset it falls back to a non-functional zimmer.example.com placeholder and generated links break. The shipped deploy sets it in config/deploy.{production,staging}.yml; a self-hosted instance must set it to its own host✅ (Kamal)
RAILS_MASTER_KEYRails credentials✅ in a self-hosted production config; on staging it is optional, and degrades silently when absent
SLACK_BOT_TOKENSlack triggers, the channel picker, and AlertServicevia mcp_secrets (encrypted credentials); ENV is the fallback
ENG_ALERTS_SLACK_CHANNEL_IDthe channel AlertService posts tovia mcp_secrets; ENV is the fallback
OPERATOR_SLACK_USER_IDthe Slack user AlertService.dm_operator DMs for conditions only a human can clear. Nothing calls it today — the needs_reauth alert is a Trigger that spawns an agent holding the Slack MCP server, which finds its own recipient. Unset means any future operator DM is logged and dropped; everything else keeps working. The bot needs the im:write scopevia mcp_secrets; ENV is the fallback
ALERTS_ENABLEDwhether this instance may post to that channel. Unset means production and staging onlyholding the token is not authorization to page. Set true on another instance that should page, false to mute one that otherwise would. ENV only — never put it in mcp_secrets, which is copied into every agent clone’s .envnot set; the default is the environment
SLACK_BOT_MENTION_ALLOWED_USER_IDScomma-separated Slack user IDs allowed to fire bot_mention, dm_message and passive-listening triggers. Blank or unset means everyone — see the caveatvia mcp_secrets; ENV is the fallback
ZIMMER_ADMIN_USERthe users.key of the human responsible for web UI actions, since Zimmer has no login. Unset falls back to tadasant; a value naming no row means web-UI human messages record nothing rather than guessing an authornot set; the default is the seeded admin

The env, secrets, and data-store wiring all live in config/deploy.*.yml and .kamal/secrets.*, not in Terraform — Terraform only provisions the host.

VarPurpose
ANTHROPIC_API_KEYClaude Code, when not using OAuth
ANTHROPIC_BASE_URLTest-only; triggers reading the OAuth token off disk and passing it as an API key
CODEX_HOMECodex config dir. Default ~/.codex
CLAUDE_CONFIG_DIRLogin isolation only (a scratch dir during the login flow)
AIR_CONFIGWhich air.json to resolve. Always wins over the per-environment default.
AIR_CATALOG_REFStaging-only catalog pinning
ELICITATION_EXPIRATION_MINUTESHow long a new elicitation (an MCP server’s approval request) stays answerable, in minutes. Default 60. An MCP server that sends its own _meta["com.pulsemcp/expires-at"] keeps it; this sets the default for everything else. Blank is treated as unset; a non-numeric or zero/negative value is logged and ignored; anything above the 7-day ceiling is clamped
X_OAUTH_REDIRECT_URIThe callback XOauthBootstrap sends on both the consent request and the token exchange. Default http://localhost:8080/callback. Whatever you set must already be registered on the X app — that registration is a manual step on X’s developer portal
VarDefault
AGENT_CLONES_DIR~/.zimmer/clones
AGENT_SCRATCH_DIRper-session durable scratch
REPO_BASE_PATHtmp/repos (bare repos)
EXECUTION_REPOS_DIR

WEB_CONCURRENCY, RAILS_MAX_THREADS, REDIS_POOL_SIZE, RAILS_LOG_LEVEL, PIDFILE, PROCESS_*.

Worker concurrency is per queue: GOOD_JOB_AGENTS_THREADS, GOOD_JOB_POLLERS_THREADS, GOOD_JOB_TRIGGERS_THREADS, GOOD_JOB_AUTH_THREADS, GOOD_JOB_DEFAULT_THREADS. Each of those threads can hold a database connection for the whole life of a job, so they size the ActiveRecord pool too — raising one raises the number of connections the database must be able to serve. DB_POOL and CABLE_DB_POOL override the derived pools directly, but read the connection budget before you do.

SENTRY_DSN_BACKEND, OTEL_SERVICE_NAME, OTEL_LOGS_EXPORTER_ENDPOINT, OTEL_LOGS_EXPORTER_BEARER_TOKEN.

Consumed as ${VAR} placeholders in mcp.json, resolved by SecretsLoader at prepare time: FLY_IO_API_TOKEN, OP_SERVICE_ACCOUNT_TOKEN, GITHUB_API_TOKEN, …

SecretsLoader resolves in this order: XOauthTokenVendor → Rails credentials (mcp_secrets) → ENV.

/settings writes to a single AppSetting row:

  • Default runtime (claude_code | codex) and default model.
  • Extension toggles — the extension_states JSONB map. See Extensions.
  • Catalog refresh controls.
LimitValue
Prompt max length500,000 chars
Session notes max50,000 chars
Search query max1,000 chars
MCP servers per session50
Skills / hooks per session100 each
Plugins per session50
API paginationdefault 25, max 100
MCP server startup timeout180,000 ms (3 min)
Elicitation expiry ceiling (ELICITATION_EXPIRATION_MINUTES)7 days
needs_input push debounce60 seconds
Trash retention (dirty clones)4 days
Large-prompt stream-json threshold100 KB