Reference: config.yaml
Every top-level block, what it's for, and what happens when it's absent. Copy config/config.example.yaml to config/config.yaml to start — every block below except organization, providers and defaults is optional and off by default. Which blocks (and which environment variables) turn a capability on, what appears when one does, and what it costs to run is the matrix in Turn features on and off.
| Block | Purpose | When absent |
|---|---|---|
organization | The GitHub organization (or user) this installation serves — the account its GitHub App is installed on. Names the shared memory scope (org:<organization>) and the About block every model run carries | required — nothing in the code assumes an organization |
providers | Model providers (anthropic, openai-compatible + a baseUrl) and which env var holds each one's key | required — nothing to route to |
defaults | agent used with no other signal; models/efforts per agent (efforts.general also controls the operator turn on models.general); verbosity — how much the bot says about its own doing, quiet (default) | verbose | debug (routing-and-config item 28); an optional boundary capping every run's budget, identity and machine class | required — the built-in agent floor is the last resort, not a real default; no boundary caps nothing |
channels / users | Static per-scope defaults, keyed by platform-namespaced id (slack:C…, slack:U…): agent, model/models, effort/efforts, verbosity, harness (which harness drives each preset's runs requested in that scope — pi or opencode, over the top-level harness block; a user's word moves that person's runs alone — see Put a preset on OpenCode), instructions, mcpServers; a scope's boundary intersects with the others' (it can only tighten) — see Restrict who can do what | that scope has no static defaults; runtime config set still applies |
grants | What each actor holds — actions (commands, agent:run:<name>, dispatch), channels whose runs it may read, repo memory it owns (repos) and independent code access (codeRepos, omitted inherits repos) — keyed by platform-namespaced id, or <ns>:* for everyone authenticated on a surface (access:* is the org) | nobody is an admin; Slack users hold the open chat commands and every unrestricted agent, browser sign-ins every read and unrestricted agents, both human baselines open code repos, credentials nothing — see reference: authorization |
restrict | Agents and repos closed to everyone not granted them | nothing restricted — human baselines admit unrestricted agents and code repos; credentials and jobs still need explicit grants — see reference: authorization |
execution | Where tool calls actually run: local (bot host), e2b / cloudflare (per-thread sandbox), plus an optional resident block for always-warm per-repo environments | local — fine for dev, not for untrusted users reaching coding |
workspaceDir | Where local-execution workspaces live on disk | ./workspaces |
memory | Memory that outlives a run: read/write to a durable store, per-scope budget; model/effort pick the reflection call's model and its effort level (card-applied; defaults: the run's own model, the model's own effort) | off — model input is byte-identical to memory disabled |
selfImprovement | The friction → GitHub-issue pipeline over run history: target repo, label, thresholds | friction propose refuses (no target repo); friction report still works over runHistory |
schedules | Where the /runs Scheduled panel reads cron firing history from | the panel lists schedules with no firing history |
ship | agent:ship pipeline caps: maxRounds, and maxMinutes — the ship preset's declared budget, which a scope's boundary or a budget: directive clips per run | sane built-in defaults (3 rounds, 240 min) |
spawn | The fan-out cap a spawning run meets: maxChildren live children per run (at least 1) — an agent:conductor spawn past it is refused until a child finishes | 3 |
routing | The one door's operator mode: on runs its typed decision, shadow records the same turn without executing it, off is the rollback lever. The retired readers' classifier has no model, effort, auto or answer key | on |
intake | The thread-reply gate's defaults layer (record 0058; routing-and-config item 27): threadReplies is how an unmentioned reply in a thread the bot is part of is treated — mention (only a mention is answered), classify (one cheap model turn decides whether the bot was addressed; the code's default) or always (today's answer-every-reply path, byte for byte) — also settable per channel and per user (intake.threadReplies on their scopes); model is the verdict's <provider>/<model> (default defaults.models.general); effort is that model's effort level, card-applied, default the model's own. A model whose operator card declares it can answer neither a forced tool call nor the text contract is refused at load when the default mode is classify. Nothing calls the gate yet — the Slack wiring lands in a later release | the default mode is classify once the gate is wired; until then behavior is unchanged |
harness | Which harness each preset's runs are driven by, deployment-wide: a mapping of preset to pi or opencode, the names the harness objects declare — see harness.md item 8 and Put a preset on OpenCode. This block is the defaults layer of the per-scope harness setting: a channel's or a user's harness (static under channels/users, or config set channel|me --harness.<preset> …) overrides it for the runs in that scope, user over channel over here; defaults.harness is refused pointing at this block. A run keeps the harness it started on, so changing a word moves the next run, never one in flight; any other word fails the load naming the two | every preset on pi — nothing defaults to OpenCode |
pi | What the harness writes into pi's per-run settings for every run on pi: compaction.reserveTokens and compaction.keepRecentTokens, pi's own thresholds in tokens — a reserve near the model's window makes a short run compact, which a receipt or a test of the compaction path wants — see harness-pi.md item 4 | pi's own defaults (16384 reserved, 20000 kept); the settings file is byte-identical to before the block existed |
opencode | What the harness writes into OpenCode's per-run configuration for every run on OpenCode: compaction.buffer and compaction.keepTokens, OpenCode's own thresholds in tokens — see harness.md item 8 | OpenCode's own defaults; the configuration file names no compaction |
costs | /costs dashboard: Cloudflare account + token, optional Anthropic admin key, named groups of Workers/containers/DOs to price; snapshot.everyHours (default 24, 1–168) is how often the billing sources are read into the snapshot the page serves; snapshot.alertChannel (optional, slack:C…) is told after three failed takes in a row and when one lands again; prices (optional) maps <provider>/<model> → { input, output, cacheRead, cacheWrite } in USD per million tokens, over the built-in Anthropic list — the table that prices a run's tokens on the costs page and on its own page (costs.md item 4b) | /costs refuses to start — nothing to report on |
delivery | /delivery page and delivery report: the repositories the page serves, the identities the indicators judge by (the review agent's login, agent logins, agent co-author names) and how often each repository's snapshot of GitHub's facts is refreshed (snapshot.everyMinutes, default 60); needs a GitHub credential; the snapshot lives on the state Worker any *.worker block names | /delivery answers 503 naming the block; delivery report --repo owner/name still works |
dashboard | Dashboard authentication: auth: access (the Cloudflare Access JWT, ACCESS_TEAM_DOMAIN + ACCESS_AUD), token (Authorization: Bearer from the env var token.env names, default DASHBOARD_TOKEN, resolving to the one actor token.actor — access:<name>) or none (loopback callers on a localhost deployment only; a public PUBLIC_BASE_URL refuses to start) — see dashboard routes | access when both ACCESS_* are set, else none — a deployed installation is unchanged; a localhost one without Access now admits its loopback callers |
slack.catchUp | Reconnect catch-up window after a deploy/drain | on, 30-minute window |
slack.relayApps | The Slack apps (by bot_id, B…) whose Sent by Claude in <#C…> on behalf of <@U…> relay footer names the person a run is for and billed to — see slack-channel.md item 13; any other app's post is the app's own request, footer or not | no footer is honoured — every app-posted request is the app's own (slack:bot:B…) |
runtimeOverrides | Where chat-set overrides (config set, config instructions) persist | data/overrides.json on host disk — ephemeral on Cloudflare Containers |
runHistory | Durable run records: retention window, byte/count caps, which store backs it | off — runs are live-only, evicted ~60s after finish |
tracing | Span log verbosity: log: roots (one JSON line per request) or slow (plus every span of 1 s or more) | roots |
Two blocks that matter most for "does a restart lose anything"
runtimeOverrides.worker and runHistory.worker (and memory.worker, schedules.worker) all point at the same state Worker (deploy/cloudflare-memory/) with the same bearer. Set them, and a bot restart loses nothing durable: overrides, memory, and run history (the friction ledger reads it) all live on Durable Objects, not the container's disk. Skip them (the default on a fresh clone), and all three fall back to a host-disk file or in-memory store — fine for local dev, silently ephemeral on a platform with no persistent disk. See explanation: Worker topology.
Full annotated example
config/config.example.yaml in the repo root is the living reference — every key, every default, every caveat, as a comment next to the setting it documents. This page is the map; that file is the terrain.
Optional account verification
dashboard.identityLink supplies slackClientId, slackTeamId and slackClientSecretEnv to the configured account-verification handler. All three must be nonblank; the secret remains in the named environment variable. The browser host registers the handler. Account changes require human Cloudflare Access authentication, the configured organization and directory; an explicit token or none strategy is refused. Register /settings/identities/callback with the Slack app before use. Missing prerequisites remain unavailable, and configuration alone does not prove a live account operation.