# Configuration

> Two-tier YAML config, environment variables, the config subcommand, and every key CommitBrief reads at runtime.

CommitBrief docs · v1.x · Configuration

Canonical URL: https://commitbrief.com/docs/1.x/configuration

---

CommitBrief uses a two-tier YAML configuration with field-level
merge — `config.Default()` skeleton → user config → repo-local
config → environment variables → CLI flags. Each layer overrides
the previous **per-field**, not whole-file.

## Files

| Tier | Path | Notes |
|------|------|-------|
| User-level | `~/.commitbrief/config.yml` | Defaults that apply everywhere. Override path with `COMMITBRIEF_CONFIG`. |
| Repo-local | `<repo>/.commitbrief/config.yml` | Per-repo overrides. Gitignored — `.commitbrief/` is auto-added to repo `.gitignore` on first write. |

Either file may be absent; a built-in skeleton fills the gap so a
fresh `commitbrief setup` works without preexisting files.

## The schema

```yaml
version: 1                                # config schema version
provider: anthropic                       # active provider
providers:
  anthropic:
    api_key: sk-ant-...                   # secret; mode 0600 on disk
    model: claude-opus-4-8
  openai:
    api_key: sk-...
    model: gpt-4o
    pricing:                              # optional: override built-in $/1M rates (OQ-09)
      gpt-4o:
        input_per_1m: 2.50
        output_per_1m: 10.00              # omitted/zero fields keep the built-in value
        cached_input_per_1m: 1.25
  gemini:
    api_key: AIza...
    model: gemini-2.5-pro
  deepseek:
    api_key: sk-...                       # + mistral, cohere (OpenAI-compatible)
    model: deepseek-chat
  ollama:
    base_url: http://localhost:11434
    model: qwen2.5-coder:14b
  claude-cli:
    model: ""                             # ignored — host CLI manages selection
  gemini-cli:
    model: ""
  codex-cli:
    model: ""                             # ignored — host CLI manages selection
output:
  lang: en                                # supported: en | tr
  color: auto                             # auto | always | never
cache:
  enabled: true                           # false skips reads + writes entirely
  ttl_days: 7                             # entry expiry; 0 → DefaultTTL (7 days)
  max_size_mb: 0                          # on-disk cap; 0 = unlimited; >0 evicts oldest first
guard:
  secret_scan: true                       # false disables the credential scanner
  secret_patterns:                        # additive user regexes on top of the built-ins (config-file only)
    - name: Acme Internal Token
      pattern: 'acme_[A-Za-z0-9]{32}'
  injection_scan: true                    # scan a custom COMMITBRIEF.md/OUTPUT.md for prompt-injection phrasing (warn, never abort)
  token_preflight: false                  # opt-in: confirm/abort when the prompt exceeds the model's context window
review:
  flaky: true                             # deterministic flaky-test detector; false disables (= --no-flaky)
  sandbox_rerun: 0                        # opt-in flaky sandbox-rerun: re-run a flagged test N times to confirm; 0 = off (= --sandbox-rerun[=N])
  sandbox_command: []                     # argv (never a shell string) that re-runs one test; templated over {{.File}} {{.Line}} {{.Test}}
                                          # e.g. ["go", "test", "-count=1", "-run", "^{{.Test}}$", "./..."] — config-file only
  baseline: true                          # honor the user-private .commitbrief/baseline.json; false disables (= --no-baseline)
  architecture: true                      # architecture-aware review: read architecture.json into the prompt; false disables (= --no-architecture)
  architecture_file: ""                   # override the architecture.json discovery path; empty = auto-discover at repo root
  timeout: ""                             # bound the whole run AND raise the provider's own cap: "10m", "90s", "600"; empty/"0" = built-ins (= --timeout)
cost:
  warn_threshold_usd: 0.50                # prompt-or-abort above this; <=0 disables
command:
  default: ""                             # args for a bare `commitbrief`; empty = --staged
```

## Per-field reference

| Dotted path | Type | Default | Notes |
|------------|------|---------|-------|
| `provider` | string | `anthropic` | Active provider. Must match a registered provider name. |
| `providers.<name>.api_key` | string | `""` | Provider API key. Not used by `ollama`, `claude-cli`, `gemini-cli`. |
| `providers.<name>.model` | string | `""` | Provider-specific model. Empty → provider's `DefaultModel()`. |
| `providers.<name>.base_url` | string | (provider default) | Override the API endpoint. Used by `ollama` (`http://localhost:11434`) and the OpenAI-compatible providers. |
| `providers.<name>.pricing.<model>.input_per_1m` | float | (built-in) | Override the input rate ($/1M tokens) for one model. Zero/omitted → built-in snapshot. |
| `providers.<name>.pricing.<model>.output_per_1m` | float | (built-in) | Override the output rate ($/1M tokens). |
| `providers.<name>.pricing.<model>.cached_input_per_1m` | float | (built-in) | Override the cached-input rate ($/1M tokens). |
| `output.lang` | string | `en` | AI **output** language — any recognized language (e.g. `en`, `tr`, `fr`, `de`, `ja`). The CLI **interface** localizes only for `en`/`tr` (English otherwise), so `--lang fr` gives a French review with an English interface. Resolution: `--lang` → repo → user → English; invalid/empty falls through. Validated by `config set`. |
| `output.color` | string | `auto` | `auto` / `always` / `never`. `--color` flag overrides. |
| `cache.enabled` | bool | `true` | `false` skips both reads and writes. |
| `cache.ttl_days` | int | `7` | Days until expiry. `0` → 7 days. Cannot be negative. |
| `cache.max_size_mb` | int | `0` | On-disk cache cap in MiB. `0` = unlimited. When `>0`, each write past the cap evicts the oldest entries first (the just-written entry is never evicted). Cannot be negative. Inspect with `cache stats` / `cache inspect`. |
| `guard.secret_scan` | bool | `true` | `false` disables the credential scanner. |
| `guard.secret_patterns` | list | `[]` | Additive user regexes (`name` + `pattern`) layered on top of the eight built-ins (v1.7.0). Built-ins always run and can't be shadowed; an invalid regex aborts before the provider call. Config-file only — `config set` rejects it. See [Safety and cost](/docs/1.x/safety-and-cost#adding-your-own-patterns-v170). |
| `guard.injection_scan` | bool | `true` | When `true` (v1.7.0), a custom `COMMITBRIEF.md`/`OUTPUT.md` is scanned for prompt-injection phrasing; a match prints a **non-blocking** warning and the review continues. See [Safety and cost](/docs/1.x/safety-and-cost#prompt-injection-scan-of-your-rules-v170). |
| `guard.token_preflight` | bool | `false` | Opt-in (v1.4.0). When `true`, a review whose estimated prompt exceeds the model's context window prompts (TTY) or aborts (non-TTY) before the paid call. See [Safety and cost](/docs/1.x/safety-and-cost#token-preflight-opt-in). |
| `review.flaky` | bool | `true` | Deterministic flaky-test detector (v1.7.0). When `true`, a provider-free pre-pass flags timing/randomness anti-patterns in changed tests and merges them into the findings. `false` (or per-run `--no-flaky`) disables it. API providers only. See [Flaky-test detector](/docs/1.x/flaky-tests). |
| `review.sandbox_rerun` | int | `0` | Opt-in sandbox-rerun confirmation for the flaky detector (v1.12.0). `0` = off; `>0` re-runs each statically flagged test in isolation that many times and classifies it (mixed → confirmed flaky, all-fail → real failure, all-pass → demoted to `info`). Per-run `--sandbox-rerun[=N]` overrides (bare flag = 5). Inert on its own — it needs `review.sandbox_command` too. See [Flaky-test detector](/docs/1.x/flaky-tests#sandbox-rerun-confirmation-opt-in). |
| `review.sandbox_command` | list | `[]` | The argv that re-runs one flagged test (v1.14.0). A **list of argv elements, never a shell string**; each element is a Go `text/template` over `{{.File}}`, `{{.Line}}`, `{{.Test}}`, handed straight to `exec.CommandContext` — no shell, so no quoting or injection surface. Arms code execution only together with a positive `review.sandbox_rerun`. Config-file only — `config set` rejects it. See [Flaky-test detector](/docs/1.x/flaky-tests#sandbox-rerun-confirmation-opt-in). |
| `review.baseline` | bool | `true` | Honor the user-private `.commitbrief/baseline.json` (v1.8.0). When `true`, findings recorded by `--update-baseline` are dropped from later runs so only new ones surface. `false` (or per-run `--no-baseline`) ignores the baseline. The file is gitignored and never propagates — it can't weaken CI or hide a bug from review. See [Signal control](/docs/1.x/signal-control#baseline). |
| `review.architecture` | bool | `true` | Architecture-aware review (v1.11.0). When `true`, the repo's `architecture.json` (the [archlint](https://github.com/muhammetsafak/archlint) config) is read and a summary of its layers + allowed/forbidden import edges is folded into the review prompt. `false` (or per-run `--no-architecture`) skips it. A one-way read — CommitBrief never lints or enforces. See [Architecture-aware review](/docs/1.x/architecture-review). |
| `review.architecture_file` | string | `""` | Override the `architecture.json` discovery path (relative to the repo root, or absolute). Empty = auto-discover at the repo root. |
| `review.timeout` | string | `""` | Bound the whole run **and raise the provider's own hard cap** (v1.16.0) — the CLI tools' 5 minutes, ollama's 5, the Anthropic SDK's 10. A context deadline alone can only shorten a run, so the value is handed down to the provider too. A Go duration (`"10m"`, `"90s"`) or a bare number of seconds (`"600"`). Empty or `"0"` keeps every built-in. Per-run `--timeout` overrides, and `--timeout 0` cancels this key for one run. Validated on write. See [Timeouts](/docs/1.x/timeouts). |
| `cost.warn_threshold_usd` | float | `0.50` | Cost ceiling. `0` or negative disables. |
| `command.default` | string | `""` | Argument string applied to a **bare** `commitbrief` (no args) — e.g. `--unstaged --cli gemini`. Bypassed the moment any flag or subcommand is passed. Empty = built-in `--staged`. Whitespace-split; no shell quoting. |

## Per-model pricing override

The cost preflight, the verbose footer, and cached-cost figures use
a built-in `$/1M`-token rate snapshot for each model. When that
snapshot drifts — or you have a negotiated rate — override it
per-model under `providers.<name>.pricing.<model>`:

```yaml
providers:
  anthropic:
    pricing:
      claude-opus-4-8:
        input_per_1m: 12.00          # your negotiated input rate
        output_per_1m: 60.00
        cached_input_per_1m: 1.20
```

Each field is independent: zero or omitted fields fall back to the
built-in value, so a partial override is fine. The override shows up
in `commitbrief config show` and flows straight into the
[cost preflight](/docs/1.x/safety-and-cost#cost-preflight) estimate.
Resolved in v1.2.0 (OQ-09).

## Customizing the bare command — `command.default` (v1.3.0)

A bare `commitbrief` (no arguments) defaults to `commitbrief --staged`.
Set `command.default` to an argument string and that bare invocation
behaves as if you had typed those arguments instead:

```yaml
command:
  default: "--unstaged --cli gemini"
```

Now a plain `commitbrief` reviews your **unstaged** changes through the
`gemini-cli` provider. It is expanded git-alias style before argument
parsing, and applies **only** to the truly bare invocation — the moment
you pass any explicit flag or subcommand (`commitbrief --staged`,
`commitbrief diff …`), the default is bypassed entirely. An empty or
unset value keeps the built-in `--staged` behavior.

Because the value usually starts with `-`, pass `--` first when setting
it from the CLI (or just hand-edit `config.yml`):

```sh
commitbrief config set -- command.default "--unstaged --cli gemini"
commitbrief config get command.default
```

## Environment variables

CommitBrief reads these at startup; they override config file
values but lose to CLI flags.

| Variable | Effect |
|----------|--------|
| `ANTHROPIC_API_KEY` | Sets `providers.anthropic.api_key`. |
| `OPENAI_API_KEY` | Sets `providers.openai.api_key`. |
| `GEMINI_API_KEY` | Sets `providers.gemini.api_key`. |
| `DEEPSEEK_API_KEY` | Sets `providers.deepseek.api_key`. |
| `MISTRAL_API_KEY` | Sets `providers.mistral.api_key`. |
| `COHERE_API_KEY` | Sets `providers.cohere.api_key`. |
| `OLLAMA_HOST` | Sets `providers.ollama.base_url`. |
| `COMMITBRIEF_CONFIG` | Override the user-level config path (absolute). |
| `LANG` / `LC_ALL` | No longer used for language selection (language is config-driven: `--lang` → repo → user → English). May still affect other locale behavior. |
| `NO_COLOR` | Force color OFF regardless of `output.color` / `--color`. |
| `COMMITBRIEF_NO_COLOR` | CommitBrief-specific equivalent of `NO_COLOR`. |

## Editing config

Three ways:

### 1. Interactive — `commitbrief setup`

Rewrites the active provider's block (non-destructive for others).
`--local` writes to the repo-local file. See
[First-time setup](/docs/1.x/first-review#1-configure-a-provider).

### 2. Programmatic — `commitbrief config`

```sh
commitbrief config show                 # YAML dump, API keys masked
commitbrief config get cache.ttl_days   # read one field
commitbrief config set provider openai  # write one field
commitbrief config set providers.openai.model gpt-4o-mini
```

Type coercion + validation: booleans accept
`true/false/yes/no/1/0/on/off` (case-insensitive); integers are
bounds-checked (no negatives for cache settings); `output.color`
is enum-validated against `auto/always/never`; `provider` is
validated against the registered factory list. `version` is
rejected — it's managed by migrations.

Two keys are **rejected by `config set`** and must be hand-edited:
`guard.secret_patterns` and `review.sandbox_command`. One can
weaken secret scanning, the other can arm code execution — both
deserve the friction of opening the file.

### 3. Hand-edit

Edit the YAML files directly. Hand-edits are loaded atomically on
the next CLI invocation; no daemon to restart. File mode is
`0600`; the parent directory is `0700`.

## Inspecting the merged config

```sh
commitbrief config show
```

Dumps the resolved config as YAML with API keys masked. Useful
for "is this what I think it is?" sanity checks.

```sh
commitbrief providers list
```

Shows every configured + registered provider, marks the active
one, displays the model and a masked API-key fingerprint (or the
base URL for Ollama).

## Dry-run

To see how a real review would resolve all of the above — diff
fetch + filter + rules + prompt + cache-key compute — without
making a provider call:

```sh
commitbrief dry-run --staged --verbose
```

Reports per-layer filter counts, the merged rules source, the
resolved language, the estimated tokens + cost, and the cache key
that *would* be looked up.

## Permissions

- `~/.commitbrief/config.yml` is written `0600`. The directory is
  `0700`.
- `<repo>/.commitbrief/config.yml` is written `0600`; the
  `.commitbrief/` directory is `0700`. The repo's `.gitignore` is
  updated to include `.commitbrief/` if not already present.

API keys are masked when printed by `commitbrief config show` and
`commitbrief providers list`; they never appear in logs.

## See also

- [Review rules](/docs/1.x/review-rules) — `COMMITBRIEF.md` and
  `OUTPUT.md` (separate from `config.yml`).
- [Providers](/docs/1.x/providers) — per-provider config fields.
- [Safety and cost](/docs/1.x/safety-and-cost) — `guard.secret_scan`
  and `cost.warn_threshold_usd` in depth.