Skip to content

Configuration

brevi’s configuration is one JSON file at ~/.brevi/config.json. Mission Control creates it on first launch and writes changes from Configuration. The file is validated on load; an unknown shape or out-of-range value makes startup fail rather than run with a surprise.

The orchestrator reads no environment variables for configuration.

Every field on this page is editable from the dashboard’s Configuration page, at /config:

Page Covers
Connectors Connect/Disconnect per provider, linear.teamKeys, r2
Repositories repos
Agent agent (models, effort, command, args, Codex review)
Workers fleet, plus the enrolled workers themselves
Memory memory, plus the stored memories themselves
Orchestrator trigger, pollIntervalSeconds, restart
Server server, connect

Each card is saved explicitly, and a save sends only the fields on that card: nothing else in the file is rewritten, so two tabs (or a tab and your editor) cannot clobber each other. Fields with a non-empty default show it as placeholder text and offer a reset once the value differs from it. Invalid input is rejected with the same message the schema would produce, and the file on disk is never left invalid: it is validated in full, then replaced atomically (and left readable only by you).

Almost everything applies to the next run without a restart. Four fields are read once at startup and are marked with a restart badge in the UI: server.port, server.host, fleet.host, and fleet.port.

One group of fields is deliberately not editable there:

  • Credentials. They are masked whenever the config is read back (see Redaction), so they are never rendered as editable values; use the Connect and Disconnect buttons instead. connect.linearClientSecret is the one write-only field: it can be replaced, never read back. linear.refreshToken and linear.tokenExpiresAt are owned by the OAuth flow.

sandbox.concurrency, sandbox.timeoutMinutes, and sandbox.retentionHours have no dashboard card (the Sandbox page is gone). Edit them in ~/.brevi/config.json; they apply live.

Hand edits keep working. The file stays the source of truth in both directions: brevi watches it and picks up an external edit without a restart, and the dashboard updates by itself. An edit that fails validation is logged and ignored, leaving the running settings alone.

A freshly initialised config, with every default filled in:

{
"linear": { "apiKey": "", "refreshToken": "", "tokenExpiresAt": "", "teamKeys": [] },
"github": { "token": "" },
"r2": { "bucket": "", "publicBaseUrl": "" },
"repos": {},
"agent": {
"command": "claude",
"args": [],
"orchestratorModel": "claude-fable-5",
"implementModel": "claude-sonnet-5",
"orchestratorEffort": "high",
"anthropicApiKey": "",
"claudeCodeOauthToken": "",
"codexApiKey": "",
"codexAuthJson": "",
"xaiApiKey": "",
"grokAuthJson": "",
"codexReview": true,
"reviewModel": "gpt-5.6-sol",
"reviewEffort": "high"
},
"memory": { "enabled": true, "maxEntries": 60, "maxChars": 8000 },
"sandbox": {
"concurrency": 1,
"timeoutMinutes": 240,
"retentionHours": 24
},
"fleet": {
"host": "",
"port": 4410,
"heartbeatTimeoutSeconds": 45,
"reconnectGraceSeconds": 120
},
"connect": {
"apiBase": "https://api.brevi.dev",
"githubClientId": "",
"linearClientId": "",
"linearClientSecret": ""
},
"trigger": { "label": "brevi" },
"restart": { "auto": true, "maxAttempts": 5, "probeIntervalMinutes": 15 },
"server": { "port": 4400, "host": "127.0.0.1" },
"pollIntervalSeconds": 15,
"configVersion": 1
}
Field Type Default Notes
apiKey string "" Personal API key or OAuth access token. Empty means not connected. Set it from the dashboard’s Configuration page.
refreshToken string "" OAuth refresh token captured by the Connect flow, used to rotate an expiring apiKey automatically. Empty for plain lin_api_ keys, which never expire.
tokenExpiresAt string "" ISO timestamp the OAuth access token expires at. Empty when unknown (plain API keys, or before the first refresh).
teamKeys string[] [] Restrict polling to these team keys, e.g. ["ENG"]. Empty polls all teams you can see.

Keys beginning with lin_api_ are sent as a raw Authorization header; anything else is treated as an OAuth token and sent as Bearer.

Field Type Default Notes
token string "" Token with the repo and workflow scopes. Used to list repos, clone, push, and open PRs. Empty means not connected.

Tickets will not run without it: every run pushes a branch and opens a pull request. The agent is always told to write a concise PR description.

Optional. When both fields are set, demo evidence (screenshots and recordings) from successful runs is uploaded to a public Cloudflare R2 bucket and embedded in the PR description.

Field Type Default Notes
bucket string "" Public R2 bucket evidence is uploaded to. Empty means uploads are disabled. Trimmed on load.
publicBaseUrl string "" Public base URL the bucket serves from: its r2.dev development URL, or a custom domain. Used verbatim to build the asset links embedded in PR descriptions, so it is trimmed and stripped of trailing slashes on load.

There is no credential field here. Authentication goes through the host’s wrangler CLI, the same one you use for any other Cloudflare work: wrangler login for auth, wrangler r2 object put for the upload. Connect and configure both fields from the dashboard’s Configuration page; see Connections.

At the end of a successful run, once artifacts are collected on the host, each screenshot (png/jpg) and recording (webm/mp4/mov/gif) is uploaded to <bucket>/<runId>/<name>, keyed by run id so names never collide across runs. Uploads are strictly best-effort: a failure is logged in the run’s console and never fails the run. If wrangler is logged out, missing, or either field is unset, runs behave exactly as before and evidence stays local.

Connecting from the dashboard provisions both fields automatically: it creates the brevi-evidence bucket and enables its r2.dev development URL. A bucket that already exists is reused only when it is already public (as a previous brevi installation’s would be); brevi never enables public access on a bucket it did not create, so that case fails with instructions instead. Edit the fields by hand only if you want a different bucket name or a custom domain.

repos maps a repo key to a repository. The key is what tickets route on: a repo:<key> label, a bare label, or a Linear project name. The dashboard uses the repository name as the key when you add a repo.

{
"repos": {
"brevi": {
"remote": "adapter/brevi",
"defaultBranch": "main",
"projects": ["Brevi"]
},
"web": {
"remote": "adapter/web",
"defaultBranch": "main",
"path": "/Users/you/code/web",
"devCommand": "bun run dev",
"devUrl": "http://localhost:3000"
}
}
}
Field Type Default Notes
remote string required "owner/name". Validated against that shape.
defaultBranch string "main" Cloned from, and the base branch of the PR.
projects string[] [] Linear project names whose tickets run against this repo. Matched case-insensitively; editable per repo on the dashboard’s Configuration page.
path string - Local checkout to clone from instead of the network.
devCommand string - Command that starts a dev server; makes the agent capture Playwright screenshots for the demo.
devUrl string - URL the dev server listens on, so the agent knows when it’s up and what to screenshot.
demo "always" | "auto" | "never" "auto" How much demo evidence runs capture. always is the full dev-server/screenshot flow; auto lets the agent downgrade to test output or a CLI transcript for changes with no visible surface (docs, tests, refactors); never skips the demo requirement.

A ticket that matches no mapping does not run. Add a repo:<key> label, map its Linear project, or name the project after a repo key.

The coding agent executed inside the sandbox.

Field Type Default Notes
command string "claude" The agent CLI brevi runs. It must be on the worker host’s PATH; bwrap bind-mounts that binary (and its PATH directories) into the sandbox.
args string[] [] Extra arguments. For Claude runs these are mapped onto the Agent SDK’s extra CLI arguments (--key value, --key=value, or a bare --flag each become one extra argument; positional, non-flag tokens can’t be mapped and are skipped with a run log line). For Codex/Grok they’re appended after brevi’s own, verbatim.
model string - When set, the whole run uses this one model with no subagent delegation, overriding orchestratorModel and implementModel.
orchestratorModel string "claude-fable-5" Model the main agent loop runs on (planning, review, delegation). Claude agents only.
implementModel string "claude-sonnet-5" Model for the implementer subagent that executes the coding tasks. Claude agents only.
orchestratorEffort "low" | "medium" | "high" "high" Reasoning effort for the main agent loop, passed to Claude Code as --effort. Claude agents only; the implementer subagent keeps the CLI’s default effort.
anthropicApiKey string "" Exported into the sandbox as ANTHROPIC_API_KEY.
claudeCodeOauthToken string "" Claude Code login, exported as CLAUDE_CODE_OAUTH_TOKEN.
codexApiKey string "" Exported as OPENAI_API_KEY.
codexAuthJson string "" Whole contents of ~/.codex/auth.json for a ChatGPT login; written into the sandbox and reached via CODEX_HOME.
xaiApiKey string "" Exported as XAI_API_KEY for Grok agents.
grokAuthJson string "" Whole contents of ~/.grok/auth.json for a Grok CLI login; written into the sandbox and reached via GROK_HOME.
codexReview boolean true Whether an adversarial Codex review runs after the implementation pass. Claude-primary runs only: when command is already Codex the review is skipped, so a run never reviews itself with its own provider. Also requires a Codex credential (codexApiKey or codexAuthJson); without one the review is skipped even when true. See Codex review below.
reviewModel string "gpt-5.6-sol" Model the Codex review runs on.
reviewEffort "minimal" | "low" | "medium" | "high" "high" Reasoning effort for Codex review executions, passed as -c model_reasoning_effort=<value>.

How brevi invokes the agent depends on command. Claude commands run through the Claude Agent SDK: it drives the installed claude binary inside the sandbox with the equivalent options, stream-json output, permission mode auto, --effort <orchestratorEffort>, and the implementer subagent defined programmatically rather than on the command line. Any other command (Codex, Grok) keeps the raw CLI invocation brevi has always used: <command> -p <prompt> --output-format stream-json --verbose --include-partial-messages --permission-mode auto, then --model <model>, then args. Those are Claude Code’s flags, so a non-Claude command has to accept the same shape. Auto mode runs without permission prompts while a classifier blocks actions that escalate beyond the ticket (such as exfiltrating data); it needs a model that supports it, which every default brevi model does.

Claude runs are a single agent session with delegation: the main loop runs on orchestratorModel and dispatches the coding work to an implementer subagent on implementModel (defined programmatically through the Agent SDK). Setting model disables delegation and runs everything on that one model. Commands containing codex always run single-model on model.

When codexReview is true, the primary agent is Claude, and a Codex credential is configured, an adversarial Codex review runs inside the same sandbox after the implementation pass finishes its coding phase and before the branch is pushed. The review is deliberately a cross-provider check: runs whose command is already Codex skip it (a console line notes the skip), so enabling it never multiplies a Codex-primary run’s spend. Three Codex reviewers (codex exec) run in parallel, each taking one angle: requirements coverage against the ticket, a bug hunt on the diff, and regression risk in the call sites the diff touches. All three judge the uncommitted diff against two sources of truth: the Linear ticket text and the existing codebase. A synthesis pass then verifies, dedupes, and ranks the findings into .brevi/review.md, kept with the run’s artifacts. Confirmed findings are fed back to the Claude orchestrator for a fix pass before the PR opens.

Without a Codex credential the review is likewise skipped cleanly and the run behaves exactly as before. The review is best effort throughout: a reviewer or the synthesis pass that fails or exhausts its sandbox.timeoutMinutes budget skips part or all of the review, and the run goes straight to finalizing without a fix pass instead of failing. The review roughly doubles the agent spend of a run; set codexReview: false to turn it off. Review executions appear in the run’s cost breakdown as “review (requirements)”, “review (bugs)”, “review (regressions)”, “review (synthesis)”, and the fix pass as “review fixes”.

The review runs the worker host’s codex binary inside the same bwrap sandbox. Live per-model cost sampling during Claude runs uses ccusage from the worker PATH or a host-side cache under ~/.brevi/cache/ccusage (installed with npm --ignore-scripts, never bind-mounted into the sandbox). Without it, runs still work and cost falls back to today’s end-of-run, stream-parsed behavior. Independently of live sampling, each Claude execution’s usage is archived on the host under ~/.brevi/ccusage for querying with ccusage; see Costs and usage.

At least one of the six credential fields (anthropicApiKey, claudeCodeOauthToken, codexApiKey, codexAuthJson, xaiApiKey, grokAuthJson) must be set or every run fails at startup with no agent credentials configured. Populate them from the dashboard’s Configuration page rather than by hand; the dashboard verifies keys before saving.

What brevi carries from one run to the next. Every run executes in a throwaway sandbox against a fresh checkout, so anything the agent worked out about a repository (which command actually builds it, where a concern lives, which trap cost it twenty minutes) would die with the sandbox and be rediscovered, at full token price, by the next ticket. Memories are the exception: durable facts a run records on its way out, kept on the host under ~/.brevi/memories/, one JSON file per repository.

Field Type Default Notes
enabled boolean true Inject stored memories into run prompts and harvest new ones afterwards. Off means every run starts cold.
maxEntries integer 1-500 60 How many memories are kept per repo. Once full, the least recently recorded ones are dropped.
maxChars integer 200-50000 8000 Character budget for the memories block injected into a prompt. Memories past the budget are left out of that run.

A run reads the repo’s memories before the agent starts and adds a ## Repository memories section to the prompt, next to the repository map. On the way out it reads .brevi/memories.md, the file the agent is asked to leave behind, and merges what it finds: a fact that is already known is reaffirmed rather than duplicated, and the repo is trimmed back to maxEntries. Follow-up runs do both too. The harvest happens before the branch is committed, so a run that ends with agent made no changes still contributes what it learned.

Memories are keyed by the repository’s remote (owner/name), not by the mapping key that resolved it, so repointing or reusing a repos key never hands the next run another repository’s facts. They are never global, and they never reach the sandbox as files: they exist only inside the prompt. Nothing is retrieved semantically; the most recently confirmed entries are injected verbatim until the budget runs out.

A wrong memory is worse than no memory, because every later run in that repo is handed it. The Memory page lists everything stored, with the ticket that recorded each fact and how many runs have confirmed it, and drops one (or a whole repo) on click. memories.md is also kept with the run’s artifacts, so you can see what a given run contributed.

Where runs execute. These fields are read by the worker that executes a run, not by the scheduling host: each worker resolves them from its own ~/.brevi/config.json. There is no provider switch: every worker uses bwrap. There is no Sandbox page in the dashboard; edit these in the file.

Field Type Default Notes
concurrency integer 1-16 1 How many sandboxed runs this worker executes at once. Adjustable live; takes effect immediately, no restart needed.
timeoutMinutes integer ≥ 1 240 Hard wall-clock limit applied per agent execution: the implementation pass, each of the parallel Codex reviewers, the synthesis pass, and the fix pass each get their own budget, rather than one limit for the whole run. The default 240 minutes gives each execution four hours.
retentionHours number ≥ 0 24 How many hours a finished (completed or failed) run’s sandbox disk is kept for interactive resume from the desktop app’s “Open terminal” button. 0 disables retention. A retained sandbox’s compute is stopped; it costs disk only, no memory or CPU.

A leftover sandbox.provider or other unknown sandbox.* key in an older file is stripped on load and ignored.

The fleet is the pool of worker daemons that dial into this host and execute runs. The host itself is a pure scheduler: it holds the run store, polls Linear, and opens PRs, but every run’s sandbox lives on a connected worker, never on the host process itself.

Which machines are enrolled is not configured here. Workers are runtime state, kept in ~/.brevi/fleet.json (mode 0600), and their credentials are minted rather than written by hand: the desktop app provisions a machine over SSH with a single-use pairing token, and the host stores only the sha256 of the durable credential that token buys. This section holds nothing secret; see Workers for enrolling, renaming, draining and revoking them.

Field Type Default Notes
host string "" Bind address for the worker channel’s own listener. Empty (the default) means that listener is off, so only a worker on this same machine can enroll, over the dashboard listener’s own /ws/worker upgrade. Set it, e.g. "0.0.0.0", to let workers on other machines reach the channel. Read once at startup: restart required.
port integer 1-65535 4410 That listener’s port, deliberately separate from server.port. Read once at startup: restart required.
heartbeatTimeoutSeconds integer 30-600 45 Seconds a connected worker may go silent before the host drops it from the fleet. Workers heartbeat every 15 seconds, so the floor is two intervals: a timeout close to one interval lets ordinary jitter drop a healthy worker. Dropping the worker does not by itself touch its in-flight runs; see reconnectGraceSeconds.
reconnectGraceSeconds integer 10-3600 120 How much longer, after a dropped worker’s runs go quiet, their leases are held before the host gives up on that worker: reconnecting within the window resumes those runs in place, not reconnecting requeues each one for another worker (or, if that worker already opened the run’s pull request, completes the run by adopting it instead of dispatching it twice).

The worker channel gets its own listener because the loopback management API is private to the desktop app. The worker channel authenticates (a single-use pairing token, then a durable per-worker credential), so it can safely bind wider without exposing the management surface. The fleet.host listener serves nothing but /ws/worker; every other request on it gets a 404. When it fails to bind (port in use, address unavailable), brevi logs it and keeps running without it.

The listener binds once at startup, so host and port need a restart; the two timing fields apply live. All four are editable from Mission Control’s Workers page, which also provisions remote machines over SSH. See Workers.

A queued run is placed on a connected worker by free capacity: each worker’s maxConcurrency caps how many dispatched runs it juggles at once. A worker an operator has set to draining is skipped entirely: it finishes what it already holds and is never offered anything new. When no connected worker qualifies, the run simply stays queued instead of failing; the reason (no workers connected, every worker draining, the fleet at capacity) is recorded on the run’s queueReason field and shown on its card in the dashboard until placement succeeds.

Every dispatched run carries a lease, renewed by its worker’s heartbeats. If a worker stops heartbeating, heartbeatTimeoutSeconds plus reconnectGraceSeconds after its last contact the host gives up on it: the run is marked interrupted and requeued for placement on another worker, unless the host finds that the worker already opened the run’s pull request, in which case the run is completed by adopting that PR rather than dispatched a second time. A worker that merely loses its connection to the host keeps executing whatever it was dispatched and buffers its reporting locally, replaying it (deduplicated) once it reconnects, so a host restart or a brief network drop does not truncate a run’s console. Restarting the host itself reloads the queue and every in-flight lease from ~/.brevi state, and only expires leases whose worker genuinely stopped heartbeating rather than treating a fresh boot as a mass disconnect.

Settings for the dashboard’s one-click Connect flows. Leave the whole object at its defaults unless you self-host.

Field Type Default Notes
apiBase string "https://api.brevi.dev" Base URL of the hosted OAuth backend brevi uses when no personal OAuth app is configured. Point it at your own deployment of apps/api to self-host.
githubClientId string "" Your own GitHub OAuth app (device flow enabled). When set, brevi talks to GitHub directly.
linearClientId string "" Your own Linear OAuth app. Requires the secret below.
linearClientSecret string "" Secret for that app. Redacted whenever the config is sent to the dashboard.

A self-hosted Linear OAuth app must register the redirect URI http://localhost:<port>/api/connect/linear/callback for the port in server.port. See Connections.

Field Type Default Notes
label string "brevi" Label that opts a ticket in. Matched case-insensitively.

What happens when the agent hits a provider usage limit mid-run. Instead of failing the run, brevi parks it as waiting and starts a fresh attempt once the limit lifts.

Field Type Default Notes
auto boolean true Automatically wait out agent usage limits and start a new attempt. With it off, a limited run fails immediately.
maxAttempts integer ≥ 1 5 Cap on agent executions per run, counting the first.
probeIntervalMinutes integer ≥ 1 15 Minutes between liveness probes while waiting on a limit whose reset time the agent didn’t report, and after a probe that is still limited.
Field Type Default Notes
port integer 1-65535 4400 The orchestrator serves both the API and the dashboard on this port.
host string "127.0.0.1" Legacy field retained for config compatibility. Mission Control always forces the management listener to 127.0.0.1; it cannot be exposed to the LAN.

Integer, minimum 10, default 15. How often brevi polls Linear for eligible tickets. brevi also polls immediately at startup and whenever Linear, the trigger label, or the repo mappings change.

Integer, default 1. A migration stamp brevi maintains, not a setting: there is no dashboard control for it. On load, a config file stamped below the current version has its stale stored defaults rewritten (for example, a stored 60 left over from the old pollIntervalSeconds default becomes 15) and is then stamped to the current version, so a value you chose on purpose, including 60, is kept and never migrated again.

When the config is read back over the API or the WebSocket, linear.apiKey, linear.refreshToken, github.token, all six agent credential fields, and connect.linearClientSecret are replaced with "***". Empty strings stay empty so the dashboard can distinguish “not connected” from “connected”.