Configuration
Two-tier YAML config, environment variables, the config subcommand, and every key CommitBrief reads at runtime.
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
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-3.5-flash
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
stream: true # vestigial — review output is non-streaming; currently has no effect
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
regex: '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
commit:
generate: 1 # default alternative-message count for `commitbrief commit` (= --generate/-g); config-file only
type: plain # default commit message format: plain|conventional|conventional+body|gitmoji|subject+body (= --type/-t); config-file only
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. |
output.stream | bool | true | Vestigial — review output is currently non-streaming, so this key has no observable effect. Readable/writable via config get/config set. |
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 + regex) 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. |
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. |
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. |
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. |
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. |
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. |
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. |
review.architecture | bool | true | Architecture-aware review (v1.11.0). When true, the repo’s architecture.json (the 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. |
review.architecture_file | string | architecture.json | Override the architecture.json discovery path (relative to the repo root, or absolute). Empty (the zero value) auto-discovers architecture.json at the repo root — the effective default either way. |
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. |
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. |
commit.generate | int | 1 | Default alternative-message count for commitbrief commit (per-run --generate/-g overrides). Config-file only — not exposed via config get/config set; hand-edit the YAML. |
commit.type | string | plain | Default commit message format for commitbrief commit: plain | conventional | conventional+body | gitmoji | subject+body (per-run --type/-t overrides). Config-file only — not exposed via config get/config set; hand-edit the YAML. |
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>:
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 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:
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):
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. |
Config strictness (v1.17.0)
A key config.yml has no field for — a typo like pattern: for
regex: under guard.secret_patterns, or a setting from a future
version — is now a hard error naming the offending file, the
exact dotted key, and the allowed siblings at that level:
config: /home/you/.commitbrief/config.yml: unknown key "guard.secret_patterns[0].pattern" (allowed: name, regex)
Before v1.17.0 that typo was silently discarded during load and
simply had no effect — the credential pattern it was meant to add
never ran, with no warning anywhere. When the offender is a
top-level key (cahce: for cache:), the message adds a “did
you mean” suggestion whenever one is a plausible edit away, plus a
reminder that a top-level key can hold a YAML anchor under an x-
prefix (next paragraph) in case that’s what was actually intended.
A top-level key meant to hold only a YAML anchor (for <<:
merging) is exempt when prefixed x- (the same convention
docker-compose and OpenAPI use):
x-defaults: &defaults
ttl_days: 3
cache:
<<: *defaults
enabled: true
--ignore-unknown-config downgrades the failure back to a warning
for one run — every ignored key is still named on stderr, and it
still has no effect, so this is a stopgap for “my config broke on
upgrade and I need to run something now”, not a way to silence
the check permanently. Fix or remove the offending key instead:
commitbrief --ignore-unknown-config --staged
This is a separate bug from the one v1.17.0 also fixes in config set/providers use’s write path (below) — writing to a config
file with fields it didn’t mention used to reset every one of them
to its zero value, guard.secret_scan: false included, with no
unknown key needed to trigger it. Neither bug causes the other;
both trace back to the same root cause: nothing in the old pipeline
distinguished “this key isn’t set”, “this key is set to its zero
value”, and “this isn’t a real key at all.”
Editing config
Three ways:
1. Interactive — commitbrief setup
Loads any existing config at the target path — honoring
--ignore-unknown-config if you’re recovering from a config broken
enough to need it — then decodes it into the full config struct and
re-marshals the whole file with your choices layered on top.
Fields for providers you didn’t touch this run survive intact, but
the write is still whole-file: unlike config set/providers use
(below), comments, key order, YAML anchors, and any key the schema
doesn’t recognize do not survive it. --local writes to the
repo-local file. See
First-time setup.
2. Programmatic — commitbrief config
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.
Since v1.17.0, config set and providers use patch the existing
YAML document in place instead of decoding it into the typed struct
and re-marshalling the whole thing: a write touches only the one
key it was asked to change, and comments, key order, YAML
anchors/aliases, and any key outside the typed schema (including
one an --ignore-unknown-config run just skipped) all survive
untouched. Every earlier release did the decode-and-rewrite instead,
so any field your file didn’t mention silently reset to its Go zero
value on every write — guard.secret_scan: false included — with
no warning. If you ran config set or providers use against a
hand-trimmed config on an older version, don’t assume this fix
alone repaired the file: run commitbrief config show and check
every value, or re-run commitbrief setup for a config that states
every field explicitly.
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
commitbrief config show
Dumps the resolved config as YAML with API keys masked. Useful for “is this what I think it is?” sanity checks.
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:
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.ymlis written0600. The directory is0700.<repo>/.commitbrief/config.ymlis written0600; the.commitbrief/directory is0700. The repo’s.gitignoreis 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 —
COMMITBRIEF.mdandOUTPUT.md(separate fromconfig.yml). - Providers — per-provider config fields.
- Safety and cost —
guard.secret_scanandcost.warn_threshold_usdin depth.