Configuration reference
Bubo reads one runtime config file: config/env.toml under BUBO_ROOT.
Start from the packaged template:
bubo init
# or, from a source checkout:
cp config/env.example.toml config/env.tomlconfig/env.toml is local operator state. Do not commit real tokens.
String values support environment-variable interpolation:
token = "${GITLAB_TOKEN}" # required; fails if unset
url = "${GITLAB_URL:-https://gitlab.com}" # fallback when unsetUse $$ for a literal dollar sign.
Runtime requirements
| Resource | Minimum | Recommended | Notes |
|---|---|---|---|
| CPU | 2 vCPU | 4 vCPU | Git checkout, JSON parsing, and agent subprocesses. |
| RAM | 2 GB | 4 GB | Increase with review concurrency. |
| Disk | 10 GB | 20 GB+ | Worktrees, SQLite state, logs, reports, and agent installs. |
| Network | outbound HTTPS | outbound HTTPS | SCM host and LLM provider access. No inbound port is required for polling. |
Required tools:
| Tool | Required for |
|---|---|
| Python 3.14+ | Bubo runtime. |
| Node.js | Codex or Claude CLI installs. |
git | Repository checkout over tokenized HTTPS. |
| Codex, Claude, or another CLI agent | Review execution. |
[scm]
| Key | Type | Default | Description |
|---|---|---|---|
provider | string | "gitlab" | SCM backend. Allowed: "gitlab", "github". BUBO_PROVIDER overrides this for one run. |
[gitlab]
Used when [scm].provider = "gitlab".
| Key | Type | Default | Description |
|---|---|---|---|
url | string | "https://gitlab.com" | GitLab web URL. Use your self-hosted GitLab URL when needed. |
api_url | string | "<url>/api/v4" | GitLab REST API URL for MRs, diffs, and outcomes. |
bot_username | string | unset | Bot username used to separate bot comments from developer replies. |
token | string | unset | GitLab token, api scope. Exported as GITLAB_TOKEN, GITLAB_PERSONAL_ACCESS_TOKEN, and GLAB_TOKEN, and used as the credential for the HTTPS git clone — sent as a per-call auth header, never written to .git/config. |
[github]
Used when [scm].provider = "github".
| Key | Type | Default | Description |
|---|---|---|---|
api_url | string | "https://api.github.com" | GitHub REST API base. Use https://<host>/api/v3 for GitHub Enterprise Server. The clone host is derived from it (api.github.com → github.com; GHES → its own host). |
bot_username | string | unset | Bot username used to separate bot comments from developer replies. |
token | string | unset | GitHub token with pull-request read/write access. Exported as GITHUB_TOKEN, GITHUB_PERSONAL_ACCESS_TOKEN, and GH_TOKEN, and used as the credential for the HTTPS git clone — sent as a per-call auth header, never written to .git/config. |
[review]
Controls polling, filtering, and review-output policy.
| Key | Type | Default | Description |
|---|---|---|---|
dry_run | boolean | true | Store planned findings without posting SCM comments. |
max_merge_requests_per_poll | integer | 5 | Maximum MRs/PRs queued by one poll cycle. |
max_findings_per_merge_request | integer | 5 | Maximum findings accepted from one review. Also fills {{MAX_FINDINGS_PER_REVIEW}} in the prompt. |
timeout_seconds | integer | 1800 | Per-review worker timeout. |
min_confidence | number | 0.85 | Minimum finding confidence required to plan or post. |
category_min_confidence | table | {} | Per-canonical-category confidence floors. Only raises the global floor. |
calibrate_confidence | boolean | false | Derive category floors from repository outcome history. |
calibrate_max_confidence | number | 0.97 | Maximum derived category floor. |
allowed_kinds | string array | [] | Optional allowlist matched against finding severity, category, or type. Empty means no kind filter. |
tone | string | "terse" | Comment voice. Allowed: "terse", "collaborative", "socratic", "formal", "casual". |
mode | string | "collaborate" | Surface mode. Allowed: "collaborate", "gate". |
suppress_disputed_classes | boolean | false | Suppress finding categories repeatedly disputed on the same repository. |
dispute_suppress_threshold | number | 0.5 | Dispute rate required before a category can be suppressed. |
dispute_suppress_min_samples | integer | 5 | Minimum outcome rows required before suppression can apply. |
verify_findings | boolean | false | Re-check surviving findings before planning/posting. |
verify_lenses | string array | ["correctness", "in_diff", "reproduce"] | Verification lenses, run in order. Each lens is one verifier call per finding; lenses stop as soon as the remaining ones cannot change the result. |
verify_min_votes | integer | 2 | Number of verifier votes required for a finding to survive. |
verify_confidence_floor | number | 0.6 | Minimum verifier confidence for a real vote to count. |
verify_max_findings | integer | 5 | Maximum findings verified per change. Findings beyond the cap post unverified. |
verify_timeout_seconds | integer | 300 | Timeout for each verifier call. |
verify_command | string array | [] | Verifier command. Empty reuses [agents].reviewer_command. |
reconcile_fixed_findings | boolean | false | Opt in to guarded closure of Bubo-owned cross-head findings. |
reconcile_confidence_floor | number | 0.9 | FIXED proof minimum; current-checkout path and line evidence are required. |
reconcile_timeout_seconds | integer | 300 | Per-proof verifier timeout. |
reconcile_lease_seconds | integer | 300 | Per-finding claim lease for concurrent workers. |
reconcile_command | string array | [] | Dedicated proof verifier; empty reuses verifier/reviewer. |
rereview | string | "full" | Policy for new commits after a completed review. Allowed: "full" (review the whole change again), "incremental" (only the commits since the last completed review, plus reconciliation of earlier findings), "off" (only the first commit). See Operate. |
settle_seconds | integer | 0 | Start a review only once the head has been unchanged this long, measured from when the poller first saw it. 0 starts immediately. |
Canonical categories for category_min_confidence: correctness, security,
concurrency, resource, error_handling, performance, style, docs,
test, design, naming, other.
[governance]
Controls AI-code provenance and advisory governance signals.
| Key | Type | Default | Description |
|---|---|---|---|
capture_provenance | boolean | false | Capture per-change AI-provenance bands for audit. |
ai_trailer_patterns | string array | built-in patterns | Regexes matched against commit-message lines for declared AI assistance. |
sensitive_path_globs | string array | [] | fnmatch globs for sensitive paths, such as payments/** or *.pem. |
rigor_modulation | boolean | false | Add heightened-scrutiny prompt context when a change escalates. |
escalate_bands | string array | ["likely_ai", "collaborative"] | Provenance bands that escalate. Allowed bands: unknown, likely_ai, collaborative. |
rigor_require_sensitive | boolean | true | Require a sensitive-path match before escalation. |
policy_mode | string | "off" | Advisory policy mode. Allowed: "off", "report-only", "soft". |
[poller]
Controls state location and single-MR debug targeting.
| Key | Type | Default | Description |
|---|---|---|---|
state_dir | string | "var" | Runtime state directory. Relative paths resolve under BUBO_ROOT. |
interval_seconds | integer | 900 | Review-service poll interval. |
outcome_sync_interval_seconds | integer | 3600 | Review-service outcome-sync interval. |
outcome_sync_limit | integer | 200 | Maximum findings graded by each service-owned outcome sync. |
target_merge_request_iid | integer | unset | Debug filter for one GitLab MR IID. Leave unset in production. |
[subscription_circuit]
This opt-in circuit breaker pauses reviews after a terminal subscription or quota failure. Review workers only record a signal; the persistent monitor is the sole writer of circuit state.
When enabled, discovery, reviewer invocation, and SCM writes require a fresh monitor heartbeat and a CLOSED circuit. Missing or stale monitor state fails closed.
| Key | Type | Default | Purpose |
|---|---|---|---|
enabled | boolean | false | Enable subscription pause and recovery monitoring. |
monitor_interval_seconds | integer | 15 | Monitor heartbeat interval. |
heartbeat_ttl_seconds | integer | 60 | Maximum age of a usable monitor heartbeat. |
probe_interval_seconds | integer | 300 | Initial interval before a recovery probe. |
probe_backoff_max_seconds | integer | 3600 | Maximum recovery-probe backoff. |
probe_timeout_seconds | integer | 30 | Timeout for one recovery probe. |
error_patterns | string array | built-in patterns | Terminal subscription/quota error patterns that open the circuit. |
[agents]
Controls the review agent command and agent-facing environment.
| Key | Type | Default | Description |
|---|---|---|---|
reviewer_command | string array | ["codex", "--ask-for-approval", "never", "exec", "--profile", "bubo", "--skip-git-repo-check"] | Only codex exec or claude -p is supported. Bubo enforces read-only policy; arbitrary wrappers and other CLIs are rejected. The prompt is appended as the final argument. |
llm_model | string | "gpt-6.1-sol" | Codex review model. Exported as LLM_MODEL; the review runner passes it to Codex as --model, bubo init templates it into the Codex profile, and the model metric label reads it. |
claude_model | string | "claude-opus-5-5" | Claude review model, passed as --model when reviewer_command runs claude -p (the Codex llm_model is never sent to Claude). bubo init also templates it into ~/.claude/settings.json; it is the model metric label on that path. Must be a Claude model id. |
llm_model_effort | string | "medium" | Reasoning effort — "low", "medium", or "high". Exported as LLM_MODEL_EFFORT; bubo init templates it into the agent profile. Falls back to the deprecated reasoning_effort key. |
llm_api_key | string | unset | LLM API key, exported as LLM_API_KEY. By default the review agent authenticates via its own login (written by bubo init), so the key is not injected into the agent environment. The one exception is llm_base_url, which reads the key from the environment at request time. |
llm_base_url | string | unset | Optional OpenAI Responses-compatible endpoint for the Codex reviewer (in-house gateway, proxy, local server). Chat Completions-only endpoints and Claude with this setting are unsupported. When set, bubo init points the Codex profile at it via a [model_providers] block, and the agent reads LLM_API_KEY from its environment. Exported as LLM_BASE_URL. Security: this is the one mode that exposes the key to the agent — leave unset unless you need it. |
llm_api_key_env | string | unset | Deprecated. Named an extra env var to expose the key under; the agent now authenticates via its own login. Still honored when set, but prefer removing it. |
dry_run | boolean | true | Exported as REVIEW_DRY_RUN for the agent. Posting is controlled by [review].dry_run. |
codex_profile | string | "bubo" | Legacy compatibility value. Hardened review commands ignore user profiles and generate their own policy. |
codex_sandbox | string | "read-only" | Read-only isolation is enforced; this setting cannot authorize weaker sandboxing. |
post_no_findings_comment | boolean | true | Post one change-level acknowledgement when a review finds no actionable issues. Honors [review].dry_run. |
no_findings_comment_body | string | "Automated review ran — no issues found." | Body for the no-findings acknowledgement. Empty disables the acknowledgement. |
Before rollout, record the installed version (codex --version or claude --version) and check its help. Codex must expose --ignore-user-config, --ignore-rules, --output-last-message, and --sandbox read-only in codex exec --help. Claude must support --safe-mode, --restricted, --strict-mcp-config, --mcp-config, --tools, and --permission-prompts. Compatibility is feature-based; no universal minimum version is asserted. Unsupported flags fail the review. Do not remove isolation flags to make an older CLI run.
Linux requires working Bubblewrap namespaces; macOS requires sandbox-exec. Validate the deployment host before rollout. A successful outer Bubblewrap test does not establish that Codex’s nested native sandbox works.
Prompt path note: live reviews load the trusted packaged prompts/00-meta.md, with a source-checkout fallback. Set the operator-controlled BUBO_PROMPT_SOURCE to use another trusted prompt file.
[telemetry]
OpenTelemetry measurement and cost settings. See Metrics & OpenTelemetry for the local HTML dashboard and preferences.
| Key | Type | Default | Description |
|---|---|---|---|
export_interval_seconds | integer | 30 | Metric export interval. |
emit_finding_events | boolean | true | Emit per-finding lifecycle metrics. |
emit_outcome_sync | boolean | true | Emit outcome-sync metrics. |
live_pricing | boolean | true | Price Codex runs from LiteLLM’s public price list, fetched at most once a day. Claude runs always record the cost Claude reports. |
input_per_1m | number | 0.0 | Input-token price per 1M tokens, for models the live list lacks, or for all models when live_pricing = false. |
output_per_1m | number | 0.0 | Output-token price per 1M tokens, used the same way. |
cached_input_per_1m | number | 0.0 | Cached-input price per 1M tokens, used the same way. |
[analytics]
Shared telemetry preferences. The local HTML dashboard remains available when delivery is disabled.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Set false to keep telemetry local. |
endpoint | string | built-in | Delivery endpoint. Blank disables delivery. |
api_key | string | built-in | Ingestion key. Blank disables delivery. |
[analytics].enabled is the only delivery preference. Use false in development
and test configuration. Running processes should be restarted after changing it.
[mcp_server]
Controls the bubo-mcp server. See MCP server for client setup.
| Key | Type | Default | Description |
|---|---|---|---|
transport | string | "stdio" | Allowed: "stdio", "http". |
host | string | "127.0.0.1" | HTTP bind address. Ignored for stdio. |
port | integer | 8765 | HTTP bind port. Ignored for stdio. |
bearer_token | string | unset | Required for HTTP transport. Clients send Authorization: Bearer <token>. |
[[projects]]
One block per repository.
| Key | Type | Default | Description |
|---|---|---|---|
path | string | required | Repository path, such as group/repo, group/subgroup/repo, or owner/repo. |
enabled | boolean | true | Include or skip the repository during polling. |
Direct environment overrides
| Variable | Description |
|---|---|
BUBO_ROOT | Install root. Defaults to ~/.local/share/bubo. |
BUBO_BASE_DIR | Runtime state directory. Usually derived from [poller].state_dir. |
BUBO_PROVIDER | Overrides [scm].provider for one run. |
BUBO_PROMPT_SOURCE | Overrides the prompt template path. |
BUBO_MCP_TRANSPORT | Overrides [mcp_server].transport. |
BUBO_MCP_HOST | Overrides [mcp_server].host. |
BUBO_MCP_PORT | Overrides [mcp_server].port. |
BUBO_MCP_BEARER_TOKEN | Overrides [mcp_server].bearer_token. |
Runtime files
| Path | Description |
|---|---|
$BUBO_ROOT/config/env.toml | Operator config. |
$BUBO_BASE_DIR/state/reviewer.sqlite | Review state, findings, and outcomes. |
$BUBO_BASE_DIR/work/ | Per-change worktrees. |
$BUBO_BASE_DIR/work/_mirrors/ | One bare git mirror per configured project, shared by that project’s worktrees. Safe to delete; it is re-created on the next review. Mirrors of projects removed from [[projects]] are pruned automatically. |
$BUBO_BASE_DIR/reports/ | Agent transcripts. |
$BUBO_BASE_DIR/log/ | JSON-line logs. |
$BUBO_BASE_DIR/rendered-prompts/ | Rendered review rules, inlined into every review prompt. |
$BUBO_ROOT/prompts/00-meta.md | Default prompt template. |
$BUBO_ROOT/skills/code-reviewer/ | Review skill for interactive Codex use; automated reviews do not need it. |
~/.codex/config.toml | Codex client config written by bubo init, unless skipped. |
~/.claude/settings.json | Claude settings written by bubo init, unless skipped. |
Sample config/env.toml
This sample uses GitLab, Codex, dry-run reviews, no HTTP MCP server, and local SQLite state. Change only the provider block and project paths for GitHub.
[scm]
provider = "gitlab"
[gitlab]
url = "https://gitlab.com"
api_url = "https://gitlab.com/api/v4"
bot_username = "bubo"
token = "${GITLAB_TOKEN}"
[github]
api_url = "https://api.github.com"
bot_username = "bubo"
# token = "${GITHUB_TOKEN}"
[review]
dry_run = true
max_merge_requests_per_poll = 5
max_findings_per_merge_request = 5
timeout_seconds = 1800
min_confidence = 0.85
category_min_confidence = {}
calibrate_confidence = false
calibrate_max_confidence = 0.97
allowed_kinds = []
tone = "terse"
mode = "collaborate"
suppress_disputed_classes = false
dispute_suppress_threshold = 0.5
dispute_suppress_min_samples = 5
verify_findings = false
verify_lenses = ["correctness", "in_diff", "reproduce"]
verify_min_votes = 2
verify_confidence_floor = 0.6
verify_max_findings = 5
verify_timeout_seconds = 300
verify_command = []
[governance]
capture_provenance = false
sensitive_path_globs = []
rigor_modulation = false
escalate_bands = ["likely_ai", "collaborative"]
rigor_require_sensitive = true
policy_mode = "off"
[poller]
state_dir = "var"
interval_seconds = 900
outcome_sync_interval_seconds = 3600
outcome_sync_limit = 200
# target_merge_request_iid = 123
[subscription_circuit]
enabled = false
monitor_interval_seconds = 15
heartbeat_ttl_seconds = 60
probe_interval_seconds = 300
probe_backoff_max_seconds = 3600
probe_timeout_seconds = 30
error_patterns = ["insufficient[_ -]?quota", "exceeded your current quota"]
[agents]
reviewer_command = [
"codex",
"--ask-for-approval",
"never",
"exec",
"--profile",
"bubo",
"--skip-git-repo-check",
]
llm_model = "gpt-6.1-sol"
claude_model = "claude-opus-5-5"
llm_model_effort = "medium"
llm_api_key = "${LLM_API_KEY}"
# llm_base_url = "https://llm.internal.example/v1" # Responses-compatible endpoint
dry_run = true
codex_profile = "bubo"
codex_sandbox = "read-only"
post_no_findings_comment = true
no_findings_comment_body = "Automated review ran — no issues found."
[telemetry]
# Consent is [analytics].enabled.
export_interval_seconds = 30
emit_finding_events = true
emit_outcome_sync = true
live_pricing = true
input_per_1m = 0.0
output_per_1m = 0.0
cached_input_per_1m = 0.0
[analytics]
# enabled = false # anonymous usage analytics are on by default; uncomment to opt out
[mcp_server]
transport = "stdio"
host = "127.0.0.1"
port = 8765
# bearer_token = "${BUBO_MCP_TOKEN}"
[[projects]]
path = "group/repo"
enabled = true
[[projects]]
path = "group/another-repo"
enabled = false