# Configuration reference

> Every config key with its type and default.

Dobby reads built-in defaults, then `dobby.config.json` in the code folder (it can only tighten safety keys), then your private file (`dobby config path` prints where it is). A later layer wins. Run `dobby config show` for the merged result. `dobby config set <key> <value>` changes one setting in your private file after checking it against this schema (`dobby config unset <key>` puts it back to the default); the dashboard's Settings page does the same.

## timezone

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `timezone` | string | your system's time zone | the clock Dobby reads times and daily hours in; an IANA name like Asia/Kolkata (the default is this Mac's own). |

## timezoneLabel

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `timezoneLabel` | string | its short name, like `PDT` | shown after clock times ("14:03 IST") |

## quietHours

notifications go out silent

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `quietHours.start` | integer (0–23) | `23` |  |
| `quietHours.end` | integer (0–23) | `8` |  |

## eveningPostHour

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `eveningPostHour` | integer (0–23) | `21` | the daily usage post in #system-status |

## github

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `github.owner` | string | `""` | the person whose @dobby PR chat is answered ("" = nobody) |
| `github.accounts` | list of string | `[]` | users/orgs whose repos get reviewed |
| `github.mergeMethod` | `squash` \| `merge` \| `rebase` | unset | how Dobby merges a PR (default squash) |
| `github.deleteBranch` | boolean | unset | delete the PR branch after Dobby merges it (default true) |
| `github.mirrorPublic` | boolean | unset | mirror tickets as issues on public repos too (default false: the backlog stays private until you say) |

## board

board.issues false: new tickets are not filed as GitHub issues (existing ones still sync status and close). `dobby add --no-issue` does it per ticket.

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `board.issues` | boolean | unset |  |

## clientNames

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `clientNames` | list of string | `[]` | real clients: tickets naming them are never agent work (policy rule client-name) |

## clientProjects

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `clientProjects` | list of string | `[]` | the projects those client rules apply to |

## frontDeskDenyMcp

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `frontDeskDenyMcp` | list of string | `[]` | MCP servers the front desk may never call (the owner's money, health or shopping apps) |

## watchJobs

other launchd jobs the hourly health check watches: label prefixes, and long-running ones (no pid = died)

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `watchJobs.prefixes` | list of string | unset |  |
| `watchJobs.daemons` | list of string | unset |  |

## paths

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `paths.projects` | string | `~/projects` | where project clones live |
| `paths.projectRoots` | list of string | unset | more folders that hold project clones; setup looks in these too (each must be inside your home folder) |
| `paths.data` | string | `~/.local/share/dobby` | Dobby's data dir (DOBBY_DIR still wins) |
| `paths.memory` | string | `~/.claude/projects/<your home folder>/memory` | the owner's Claude Code memory folder (dobby company) |
| `paths.usageCli` | string | `""` | the claude-code-usage-limits reader; "" = the copy bundled in node_modules |
| `paths.dashboardDist` | string | `""` | the built dashboard app (DOBBY_DASHBOARD_DIST still wins) |
| `paths.agentRules` | string | unset | your own rules file, appended to every agent's rules after Dobby's |

## tools

Agent tooling (`dobby tools`). A tool's rules reach agents only when it is on AND found on this machine. Absent = graphify, rtk, caveman, ponytail on (free); jev on only when safety.classifier is "jev" (paid: opt in).

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `tools.graphify` | boolean | unset |  |
| `tools.rtk` | boolean | unset |  |
| `tools.caveman` | boolean | unset |  |
| `tools.ponytail` | boolean | unset |  |
| `tools.jev` | boolean | unset |  |

## discord

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `discord.hq` | list of string | 4 entries | channels in the HQ category |
| `discord.apps.<name>.label` | string | unset |  |
| `discord.apps.<name>.ideas` | boolean | unset |  |
| `discord.extraChannels` | map of string to list of string | `{}` | more channels in an app's category |

## providers

Model providers and routing (docs/manual/providers.md). Absent = unused: Dobby calls claude exactly as before. strip, not strict: an unknown key here warns and is ignored like everywhere else, instead of discarding the whole file.

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `providers.enabled` | boolean | `false` | off = Dobby keeps calling claude exactly as today |
| `providers.acp` | boolean | unset | ACP harnesses (gemini-cli...) in live routing; off until verified against a real agent |
| `providers.providers.<name>.enabled` | boolean | `true` |  |
| `providers.providers.<name>.keyEnv` | list of string | unset | pool of key NAMES, values stay in the environment |
| `providers.providers.<name>.baseUrl` | string | unset | relay or custom endpoint |
| `providers.providers.<name>.headers` | map of string to string | unset |  |
| `providers.providers.<name>.strategy` | `fill_first` \| `round_robin` \| `least_used` | `fill_first` |  |
| `providers.providers.<name>.limit.dailyUsd` | number > 0 | unset |  |
| `providers.providers.<name>.limit.dailyRuns` | integer > 0 | unset |  |
| `providers.roster` | list of object | unset | replaces/extends the seed roster by name |
| `providers.main` | string | unset |  |
| `providers.roles.<name>.primary` | string | unset |  |
| `providers.roles.<name>.fallback` | list of string | unset |  |
| `providers.roles.<name>.effort` | `low` \| `medium` \| `high` | unset |  |
| `providers.roles.<name>.maxCostUsd` | number (0+) | unset |  |
| `providers.policy.allowUnsafe` | list of `unsandboxed-builders` \| `sensitive-below-top` \| `private-remote` \| `owner-data-to-providers` | `[]` |  |

## review

The PR reviewer (docs/manual/reviews.md): overrides of its DEFAULTS (everything on, safety limits kept). Absent = the defaults. provider: who gates merges (docs/manual/external-reviewer.md); absent = "local", Dobby's own reviewer.

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `review.auto` | `on` \| `drafts` \| `off` | `off` | on = auto-review every open PR, drafts too; drafts = auto-review all but drafts (what Dobby always did); off = only on request |
| `review.includePaths` | list of string | `[]` | globs; non-empty = only these files are reviewed |
| `review.excludePaths` | list of string | `[]` | globs never reviewed (lockfiles, generated code) |
| `review.branches` | list of string | `[]` | base-branch globs auto-reviewed; empty = every base branch |
| `review.skipLabel` | string | `no-review` | a PR with this label is not auto-reviewed ("" = none) |
| `review.mentionOnly` | boolean | `false` | no auto reviews; only `@<handle> review` (turns commands on) |
| `review.handle` | string | `dobby` | On a public repo @dobby may be a real GitHub user who gets pinged: pick a handle you control (a bot account, your org's name). |
| `review.botLogin` | string | `""` | also answer `@<botLogin> <verb>` (the GitHub App or bot account you review as); "" = only the handle |
| `review.commands` | boolean | `true` | PR comment commands from users with write access |
| `review.fullReviewsPerDay` | integer (0+) | `3` | `full review` per PR per day |
| `review.maxPerScan` | integer (1+) | `4` | auto reviews per review-scan |
| `review.maxChats` | integer (0+) | `3` | owner @foreman chat answers per scan |
| `review.maxCommandsPerScan` | integer (0+) | `5` | commands, thread questions and learnings handled per scan |
| `review.ask` | boolean | `true` | `@<handle> ask` and replies in our review threads get an answer |
| `review.describe` | boolean | `true` | `@<handle> describe` fills an EMPTY PR description (off: the suggestion goes in the walkthrough) |
| `review.maxFindings` | integer (0+) | `0` | 0 = no cap (ranked, most severe first) |
| `review.strictness` | `chill` \| `default` \| `assertive` | `default` |  |
| `review.riskBump` | boolean | `false` | a haiku/sonnet pick on a diff touching riskPaths becomes opus |
| `review.riskPaths` | list of string | 29 entries |  |
| `review.contextPack` | boolean | `true` | callers, tests and history of the changed files from graphify-out/graph.json, when present |
| `review.contextFiles` | integer (1+) | `10` | changed files the context pack covers |
| `review.learn` | boolean | `true` | a 👎 or "won't fix" reply on our thread = a "known intentional" hint for later reviews of the repo |
| `review.maxLearnings` | integer (1+) | `30` | kept per repo, newest first |
| `review.fix` | boolean | `true` | `@<handle> fix` queues the open high/medium findings as a builder ticket (never pushes itself) |
| `review.fixesPerDay` | integer (0+) | `1` | `@<handle> fix` per PR per day (the 2 auto-fix rounds per PR still apply) |
| `review.jevReplies` | boolean | `false` | Send replies in our threads to Jev (redacted) to tell a "won't fix" from a question. Off: no Jev call; every reply that is not a plain "thanks" is answered as a question and never becomes a learning (comment text stays on the Mac). |
| `review.repos` | map of `owner/name` to review settings | unset | the same keys as `review`, for one repo |
| `review.provider` | `local` \| `external` \| `both` | unset |  |
| `review.external` | `coderabbit` \| `greptile` \| `copilot` | unset |  |
| `review.externalCheck` | string | unset |  |
| `review.externalBots` | list of string | unset |  |
| `review.externalFix` | boolean | unset |  |
| `review.externalFixCap` | integer (0–10) | unset |  |
| `review.externalTimeoutMin` | integer (1+) | unset |  |

## ticketBudget

Token savings. Every key is absent by default = Dobby behaves as before; the private config turns each on. ticketBudget: most one ticket may spend, by size, in percent-of-the-week points (usdPerWeekPoint dollars = 1 point; default 26, measured). Over it, the ticket goes to the owner.

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `ticketBudget.S` | number > 0 | unset |  |
| `ticketBudget.M` | number > 0 | unset |  |
| `ticketBudget.L` | number > 0 | unset |  |
| `ticketBudget.usdPerWeekPoint` | number > 0 | unset |  |

## pacing

pacing.fallbackBuildsPerDay: builds started per day (configured timezone) while the Claude usage cannot be read (default 3). pacing.weekCap: most of your weekly plan Dobby may use, in percent; the rest is always yours (default 95).

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `pacing.fallbackBuildsPerDay` | integer (0+) | unset |  |
| `pacing.weekCap` | integer (10–100) | unset |  |

## scout

scout: the Talent Scout's daily run, at `scout.hour`. enabled false = never runs on its own (`dobby scout` still works); hour = when its 7-hour window opens, configured timezone (default on, 23).

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `scout.enabled` | boolean | unset | Whether the Talent Scout runs on its own each day. |
| `scout.hour` | integer (0–23) | unset | The hour its 7-hour window opens, in your time zone. |

## lead

lead.onboardPromote: what happens to the first backlog of a newly onboarded project. ask (default) = `dobby init <project>` shows the tickets the Lead would queue (up to 3) and asks; with --yes, no terminal or a scheduled start nothing is queued until you run `dobby queue <id>`. auto = the Lead queues them as before. none = never at onboarding.

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `lead.onboardPromote` | `ask` \| `auto` \| `none` | unset | What happens to the first tickets of a new project. ask: you choose which to queue. auto: the Lead queues them. none: nothing is queued. |

## stats

stats: opt-in crash reports and usage stats (docs/manual/stats.md). Both are off until you say yes; no answer is off. DO_NOT_TRACK, DOBBY_STATS=off and CI force them off. stats.crashes: send the error class, the place in Dobby's own code and the command when Dobby fails (never the message). stats.usage: send how far setup got and daily counts (what you can see with `dobby stats show`). stats.endpoint: where they go (default: the owner's collector, dobby-stats.nimitharia1.workers.dev); empty string = nothing is sent.

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `stats.crashes` | boolean | `false` |  |
| `stats.usage` | boolean | `false` |  |
| `stats.endpoint` | string | the owner's collector; `""` sends nothing |  |

## retryLimit

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `retryLimit` | integer (1+) | unset | builds one ticket may use before it goes to the owner instead of being rebuilt. |

## builderContextCeiling

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `builderContextCeiling` | integer (20000+) | unset | tokens of context at which a builder run is ended and continued in a fresh run with a short handoff. |

## shipPreflight

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `shipPreflight` | boolean | unset | before a builder starts, check gh auth, the remote, push access and the verify command; a failure blocks the ticket with the reason. On by default; false = off. |

## sameFailure

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `sameFailure` | boolean | unset | a failure with the same cause as the last build's goes to the owner instead of another build. |

## harnessInstall

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `harnessInstall` | boolean | unset | when a build changes a dependency manifest, Dobby runs the project's install step itself (with network) before verify. |

## installDomains

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `installDomains` | list of string | unset | hosts an install may reach from inside the sandbox, besides the public package registries (a private registry). |

## freshRetries

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `freshRetries` | boolean | unset | a retry or fix build starts a fresh run (Tester notes + work so far in the prompt) instead of resuming the old, large session. |

## recoverOutput

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `recoverOutput` | boolean | unset | a builder whose output cannot be read is recovered from its session transcript instead of counting as a wasted build. |

## deployOnFailure

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `deployOnFailure` | `notify` \| `ticket` \| `fix` | unset | what a failed deploy does. notify (default) = the Retry card only; ticket = also a top-priority fix ticket; fix = that ticket is built at once (`dobby run <id>`), one message, and the fix's merge deploys again. One open fix per project. |

## deployFrom

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `deployFrom` | `checkout` \| `worktree` | unset | checkout (default) = the project's deploy_dir, pulled; worktree = a fresh worktree of origin/<base> from the project's repo for each deploy (its .env* files copied in), removed afterwards, so no separate release checkout is needed. |

## mergeChecks

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `mergeChecks` | `all` \| `required` | unset | which PR checks gate a merge. required (default) = the repo's required checks only (every check but Dobby's review when it requires none); all = every check. DOBBY_CHECKS=all\|required overrides. |

## reviewCap

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `reviewCap` | number (1–100) | unset | the 5-hour % review-scan may use, when looser than the builders' pace cap (reviews unblock merges). Unset = the builders' cap, as before. The week stays under pacing.weekCap either way. |

## billing

billing: plan (default) = a Claude Pro/Max sign-in, paced by its 5-hour and weekly windows. apiKey = no plan: every run, builders included, pays by the token through DOBBY_API_KEY, capped at dailyUsd per 24 hours and weeklyUsd per 7 days (the plan limits pacing.weekCap and the 5-hour pace do not apply; over the cap, `dobby limits` says Paused). workspaceId: the Console workspace id (wrkspc_...), for an organisation-level key not scoped to a workspace; sent as the anthropic-workspace-id header. Not a secret.

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `billing.mode` | `plan` \| `apiKey` | unset |  |
| `billing.dailyUsd` | number > 0 | `20` |  |
| `billing.weeklyUsd` | number > 0 | unset |  |
| `billing.workspaceId` | string | unset |  |
| `billing.planUsd` | number > 0 | unset |  |

## costComment

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `costComment` | boolean | unset | false = no cost comment on Dobby PRs (default on). billing.planUsd = what the owner pays for the plan; only the receipt card reads it. |

## safety

Safety (README "Safety"). preset: strict (every sensitive category waits for the owner) | balanced | autonomous. rules: per-category override of the preset: auto (normal review, then merge) | review (strict Opus review) | owner (needs-you card, no fix, no merge) | never (agents do not touch it). classifier: model (default) | jev | none. jev = the TypeSafe classifier (@typesafe-ai/sdk), optional: needs JEV_API_KEY in the environment, else the model is used. none = words only. unsure: what a classifier that cannot decide means: ask (send the owner a card) | allow (go on, at most a strict review).

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `safety.preset` | `strict` \| `balanced` \| `autonomous` | `balanced` |  |
| `safety.rules` | map of `migrations.schema` \| `migrations.data` \| `payments` \| `auth` \| `secrets` \| `deploys` \| `config` \| `client-sites` \| `releases` to `auto` \| `review` \| `owner` \| `never` | `{}` |  |
| `safety.classifier` | `jev` \| `model` \| `none` | `model` |  |
| `safety.unsure` | `ask` \| `allow` | `ask` |  |
| `safety.everyMerge` | `auto` \| `review` \| `owner` | unset | everyMerge: the floor for every merge, including changes in no category: auto \| review (strict review) \| owner (draft PR, the owner merges). Unset = the preset's (strict: owner, else auto). deploy: after a merge, ask (a Deploy tap; also when unset) \| auto (deploy at once, only when you set it). |
| `safety.deploy` | `auto` \| `ask` | unset |  |

## migrated

What `dobby migrate` found in the setup it migrated (a record; nothing reads it). Secret key NAMES only. launchdJobs is the file names of the com.dobby.* and com.foreman.* launch agents that were in ~/Library/LaunchAgents when it ran; Dobby does not install, remove or read them from here (`dobby jobs` does that).

| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `migrated.at` | string | unset |  |
| `migrated.projects` | list of string | unset |  |
| `migrated.secretKeys` | list of string | unset |  |
| `migrated.discord` | map of string to string | unset |  |
| `migrated.launchdJobs` | list of string | unset |  |
