Skip to content

Run it locally

  • Ruby 3.4.6 (see .ruby-version)
  • PostgreSQL 14+
  • Redis
  • Node.js (for the AIR CLI and npx-based MCP servers)
  • The claude and/or codex CLI, logged in — or pi, which takes an OPENROUTER_API_KEY in the environment instead of a login. See Runtimes
  • gh CLI, logged in (agents use it to open PRs)
Terminal window
bundle install
cp .env.example .env # then set ANTHROPIC_API_KEY
bin/rails db:setup
bin/dev # → http://localhost:3000

bin/setup does the same thing plus bundle check, db:prepare, and log:clear, then execs bin/dev. Use PORT=0 bin/dev for a random port.

bin/dev installs foreman if it’s missing, warns (non-fatally) if redis-cli ping fails, kills any stale foreman or port-3000 processes, and starts Procfile.dev:

web: bin/rails server
css: while true; do bin/rails tailwindcss:watch || sleep 5; done

config/database.yml and config/cable.yml expect two databases per environment: zimmer_development and zimmer_development_cable. The second is Action Cable’s, via solid_cable. bin/rails db:setup creates both.

Everything in .env.example is commented out except RAILS_ENV=development. The ones that matter:

VarWhat for
DATABASE_HOST / _PORT / _USERNAME / _PASSWORDPostgres. Postgres.app users want 5450.
REDIS_URLCache. redis://localhost:6379 — the server, with no database index. Each environment picks its own (development’s cache takes 1, production 0).
ANTHROPIC_API_KEYClaude Code, if not using OAuth
API_KEYSComma-separated keys for the REST API. Unset ⇒ the API 401s on everything.
APP_HOSTThe MCP OAuth redirect host. Unset ⇒ defaults to localhost:3000.
RAILS_MASTER_KEYUnlocks Rails credentials (mcp_secrets, mcp_oauth_clients)
AIR_CONFIGOverride which air.json the catalog resolves from
AGENT_CLONES_DIRWhere session clones go. Default ~/.zimmer/clones
GOOD_JOB_AGENTS_THREADSWorker concurrency: how many agent sessions run at once. It also moves the database pool that has to serve them — see the connection budget
GOOD_JOB_MAINTENANCE_THREADSWorker concurrency for blocking package, archive, and filesystem maintenance. It also moves the database pool.

gh and the agent CLIs authenticate via OAuth (gh auth login, claude auth login), not env vars.

On boot, config/initializers/air_catalog.rb runs AirCatalogService.refresh!, which lazily npm installs the AIR CLI (pinned to 0.13.0) into AIR_INSTALL_DIR and then shells out to air resolve. The first boot is slow because of that install.

AIR_INSTALL_DIR defaults to /opt/air-cli where that exists (the Docker image bakes it there) and ~/.cache/air-cli otherwise. The test environment is deliberately separate — always ~/.cache/air-cli-test — because test/test_helper.rb installs the CLI at suite boot, and on a host that is also running Zimmer that would reinstall over the running app’s copy. Set the variable explicitly to override either.

If the catalog fails to resolve, the app downgrades to a warning and serves a stale snapshot — but the test suite is less forgiving. See below.

Terminal window
bin/rails test test/models/session_test.rb # targeted — do this
bin/rails test # the whole suite
bin/rubocop # lint
bin/brakeman # security scan

Run targeted tests locally and let CI run the full suite.

.github/workflows/ci.yml runs the Chrome-driven system suite (test/system/*.rb) in a dedicated test-system job, alongside the unit + integration test-unit job. The JavaScript Playwright scripts under test/e2e/*.js are the one browser layer still not wired in (issue #162) — see Testing philosophy.