Configuration reference
The config files
Section titled “The config files”| File | What it is | Read by |
|---|---|---|
air.json | The AIR catalog wiring (dev/test) | AirCatalogService via the AIR CLI |
air.production.json | Same, for the in-image catalog (prod/staging) | ditto |
roots.json | Agent root definitions | AgentRootsConfig |
mcp.json | MCP server registry | ServersConfig |
skills/skills.json | Skill index | SkillsConfig |
plugins/plugins.json | Plugin index | PluginsConfig |
hooks/hooks.json | Hook index | HooksConfig |
references/references.json | Reference index | ReferencesConfig |
config/goals.json | Goal / stop-condition catalog | GoalsConfig |
config/goals.json is the exception — it’s a plain static file that GoalsConfig reads directly, outside AIR.
Environment variables
Section titled “Environment variables”Required in production
Section titled “Required in production”| Var | Purpose | Set by the shipped deploy? |
|---|---|---|
SECRET_KEY_BASE | Rails secret | ✅ (Kamal) |
DATABASE_HOST / _PORT / _USERNAME / _PASSWORD / _SSLMODE | Postgres | ✅ (Kamal) |
REDIS_URL | Cache | ✅ (Kamal) |
API_KEYS | REST API auth | ✅ (Kamal) |
SUPERVISOR_PASSWORD | The 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_HOST | MCP OAuth redirect URI, and the mailer link host | ✅ (Kamal) |
ZIMMER_PROD_BASE_URL / ZIMMER_STAGING_BASE_URL | Externally-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_KEY | Rails credentials | ✅ in a self-hosted production config; on staging it is optional, and degrades silently when absent |
SLACK_BOT_TOKEN | Slack triggers, the channel picker, and AlertService | via mcp_secrets (encrypted credentials); ENV is the fallback |
ENG_ALERTS_SLACK_CHANNEL_ID | the channel AlertService posts to | via mcp_secrets; ENV is the fallback |
OPERATOR_SLACK_USER_ID | the 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 scope | via mcp_secrets; ENV is the fallback |
ALERTS_ENABLED | whether this instance may post to that channel. Unset means production and staging only — holding 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 .env | not set; the default is the environment |
SLACK_BOT_MENTION_ALLOWED_USER_IDS | comma-separated Slack user IDs allowed to fire bot_mention, dm_message and passive-listening triggers. Blank or unset means everyone — see the caveat | via mcp_secrets; ENV is the fallback |
ZIMMER_ADMIN_USER | the 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 author | not 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.
Agent + tooling
Section titled “Agent + tooling”| Var | Purpose |
|---|---|
ANTHROPIC_API_KEY | Claude Code, when not using OAuth |
ANTHROPIC_BASE_URL | Test-only; triggers reading the OAuth token off disk and passing it as an API key |
CODEX_HOME | Codex config dir. Default ~/.codex |
CLAUDE_CONFIG_DIR | Login isolation only (a scratch dir during the login flow) |
AIR_CONFIG | Which air.json to resolve. Always wins over the per-environment default. |
AIR_CATALOG_REF | Staging-only catalog pinning |
ELICITATION_EXPIRATION_MINUTES | How 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_URI | The 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 |
| Var | Default |
|---|---|
AGENT_CLONES_DIR | ~/.zimmer/clones |
AGENT_SCRATCH_DIR | per-session durable scratch |
REPO_BASE_PATH | tmp/repos (bare repos) |
EXECUTION_REPOS_DIR | — |
Concurrency and logging
Section titled “Concurrency and logging”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.
Observability
Section titled “Observability”SENTRY_DSN_BACKEND, OTEL_SERVICE_NAME, OTEL_LOGS_EXPORTER_ENDPOINT,
OTEL_LOGS_EXPORTER_BEARER_TOKEN.
MCP server secrets
Section titled “MCP server secrets”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 you change in the UI
Section titled “Settings you change in the UI”/settings writes to a single AppSetting row:
- Default runtime (
claude_code|codex) and default model. - Extension toggles — the
extension_statesJSONB map. See Extensions. - Catalog refresh controls.
Hard-coded limits
Section titled “Hard-coded limits”| Limit | Value |
|---|---|
| Prompt max length | 500,000 chars |
| Session notes max | 50,000 chars |
| Search query max | 1,000 chars |
| MCP servers per session | 50 |
| Skills / hooks per session | 100 each |
| Plugins per session | 50 |
| API pagination | default 25, max 100 |
| MCP server startup timeout | 180,000 ms (3 min) |
Elicitation expiry ceiling (ELICITATION_EXPIRATION_MINUTES) | 7 days |
needs_input push debounce | 60 seconds |
| Trash retention (dirty clones) | 4 days |
| Large-prompt stream-json threshold | 100 KB |