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 and the mutating POST /health/* maintenance actions. Fails closed — unset or blank means every Administrate dashboard and the health page’s Clean Up / Retry / Trash / Halt queues / Re-arm buttons return 401, including for you. The read-only health dashboard, /up, and POST /health/exit_queue_recovery_mode stay open regardless, and every gated action has a POST /api/v1/health/* sibling behind API_KEYS. 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 and the channel pickervia mcp_secrets (encrypted credentials); ENV is the fallback
ENG_ALERTS_SLACK_CHANNEL_IDthe operational alert channel. Zimmer does not post there — the obs pipeline does, through its own webhook — it reads this id only to recognize the channel and keep passive listening from treating a page as a conversationvia mcp_secrets; ENV is the fallback
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_GIT_USER_NAME / ZIMMER_GIT_USER_EMAILthe user.name / user.email every agent session commits with, written into the container’s ~/.gitconfig at boot. Set both or neither — Zimmer will not invent the missing half, and without them a session’s first git commit fails with Author identity unknown (why there is no default). Every commit a session makes is authored as this identity, so pick one you are content to see in git log. See the git identity an agent session commits withnot set; sessions cannot commit until it is
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
PI_CODING_AGENT_DIRPi config dir (auth.json, models.json, settings.json). Default ~/.pi/agent, and set explicitly in Dockerfile.base so every container agrees. PiRuntimeAdapter exports it to the spawned process
PI_EXTENSIONS_DIRWhere the Pi extensions are installed. Default /opt/pi-extensions; CI runners that cannot write there redirect it
OPENROUTER_API_KEYPi’s provider credential. Normally set through the Inference page’s Pi tab rather than here — see Runtimes
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: rewrites every github://tadasant/zimmer-catalog/… URI in the in-image air.production.json to pin that ref. It is read inside the AIR_CONFIG fallback, so setting AIR_CONFIG bypasses it entirely — and air.production.json declares no github:// URIs, so as shipped it pins nothing on any deployment and staging warns at boot and resolves the catalog unrewritten. See Catalog pinning is real code for a catalog this deployment does not run
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_URIWhere X sends the operator after the consent that /supervisor starts, on both the consent request and the token exchange. Default http://localhost:8080/callback, where nothing listens, so the operator pastes the redirect URL back. Set it to https://<APP_HOST>/supervisor/x_oauth/callback and the flow finishes on its own. Whatever you set must already be registered on the X app — that registration is a manual step on X’s developer portal. See X (Twitter) is minted from /supervisor
ZIMMER_CONTENT_SEARCH_BUDGET_SECONDSWall-clock ceiling for one transcript content search (search_contents), in seconds. Default 20, chosen to sit under kamal-proxy’s 30-second target timeout — raising it past that trades a structured “scan incomplete” answer for a 504. Zero means “stop before the first chunk”; a negative value is ignored. See Searching transcript contents
ZIMMER_CONTENT_SEARCH_CHUNK_SIZEHow many sessions one content-search statement reads at a time. Default 100. Smaller chunks make the budget finer-grained at the cost of more round trips
VarDefault
AGENT_CLONES_DIR~/.zimmer/clones
AGENT_TRANSCRIPT_ARCHIVE_DIR~/.zimmer/transcript_archives — where TranscriptArchiveJob writes latest.zip. A sibling of the clones dir, and on the same zimmer_data volume, because the job runs in the worker container and every reader of the archive is an HTTP route in web. Point it somewhere both roles mount, or the reader stops seeing the writer
AGENT_SCRATCH_DIRper-session durable scratch
REPO_BASE_PATHtmp/repos (bare repos)

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_MAINTENANCE_THREADS, GOOD_JOB_TRIGGERS_THREADS, GOOD_JOB_AUTH_THREADS, GOOD_JOB_INFERENCE_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 | pi) and default model. See Runtimes.
  • 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