Configuration

Everything rupu reads from config.toml — every section and key, its type, its default, and what it does. All keys are optional: an empty file (or no file) is a working configuration.

Files and layering

rupu reads up to three TOML files and merges them, most specific last:

LayerFileUse it for
Global~/.rupu/config.tomlYour accounts, defaults, pricing, control-plane settings. RUPU_HOME moves the whole directory.
Customer~/.rupu/customers/<slug>/config.tomlPer-customer overrides for every project assigned to that customer — see Customer layer.
Project<repo>/.rupu/config.tomlRepo-local overrides, checked in with the code.

Scalars and tables from a more specific layer override the less specific one; arrays replace rather than merge (a project that declares [[scm.rules]] replaces the global list). Resolution order, highest first:

# highest wins
global lock  ›  customer lock  ›  project  ›  customer  ›  global  ›  built-in default

Sections are strict: an unknown key inside a section such as [bash], [cp] or [netflow] is a parse error rather than a silently ignored typo. A few retired keys are still accepted as no-ops — see Deprecated keys.

Environment variables

There is no environment layer — environment variables are never merged into the config. A handful are read directly by the code they affect and win over the matching key there:

VariableEffect
RUPU_HOMEMoves the global directory (default ~/.rupu): config, credentials, runs, caches.
RUPU_LOGWins over log_level.
RUPU_MAX_OPEN_FILES, RUPU_MAX_CONCURRENT_JOBS, RUPU_MIN_FREE_MEMORY_MBWin over the [runtime] keys of the same name.
RUPU_NETFLOW_SUBPROCESS=0Forces [netflow].subprocess_capture off.
RUPU_NO_UPDATE_CHECKSuppresses the passive update notice, like [update].check = false, when set to anything other than empty, 0 or false (so RUPU_NO_UPDATE_CHECK=0 leaves it on).
RUPU_LIVE_VIEW=0Turns the workflow live view off (as --plain does).
NO_COLORForces [ui].color = "never".
VISUAL / EDITORUsed when [ui].editor is unset.
RUPU_<ACCOUNT>_API_KEYAPI-key fallback for a provider account with no stored credential — see Providers.

Reading and editing

rupu config get ui.theme
rupu config set cp.gate_sweep_interval_secs 30
rupu config set update.channel beta

rupu config get / set work on the global file only. Dotted keys descend into tables; the value is parsed as a TOML scalar (string, integer or bool), and set refuses to replace a table with a scalar or the reverse. Edit arrays and tables ([[scm.rules]], [recovery].fallbacks) by hand, a customer layer with rupu customer edit <slug>, or everything from the control plane's Settings page, which shows each effective value with the layer it came from, a lock toggle, and a raw TOML editor.

Locks and policy — [policy]

KeyTypeDefaultNotes
lockarray<string>[]Dotted key paths (permission_mode, autoflow.max_active) whose value from this layer can't be overridden. A global lock beats every layer; a customer lock beats the project layer only and cannot unlock a key the global list names. A project's own lock list is ignored.
# ~/.rupu/config.toml — no project or customer can loosen these
permission_mode = "readonly"

[policy]
lock = ["permission_mode", "bash.env_allowlist"]
Scope of a lock. A lock covers config keys only. An agent whose own frontmatter names provider: or auth: still runs on that provider even when default_provider is locked.

Customer layer

Assign a project to a customer with rupu customer assign <slug> (a slug is 1–63 of a-z, 0-9, -; none is reserved). The customer's config.toml then sits between the global and project files for every run in that project — and its subdirectories, since a run uses the nearest assigned ancestor of its working directory.

# ~/.rupu/customers/northwind/config.toml
default_provider = "anthropic-northwind"

[[scm.rules]]
owner   = "northwind-labs"
account = "github-northwind"

[policy]
lock = ["default_provider"]   # a repo's .rupu/config.toml can't move these runs

More in Customers.

Top-level keys

KeyTypeDefaultNotes
default_providerstringnoneProvider account used when an agent omits provider:.
default_modelstringclaude-sonnet-4-6Used when the agent sets no model: and the provider's own default_model is unset.
permission_modestringaskask | bypass | readonly; fallback when neither the agent nor --mode sets one.
log_levelstringwarnAny tracing directive (debug, rupu_scm=debug,info). RUPU_LOG wins; blank counts as unset.

Model providers — [providers.<account>]

One table per provider account: a bare vendor name (anthropic, openai, gemini, copilot), a named account of a vendor (anthropic-work with kind = "anthropic"), or an openai-compatible endpoint you name yourself. An agent's provider: names the account. Narrative: Providers & Auth.

KeyTypeDefaultNotes
kindstringnone (the table name is the vendor)Vendor of a named account, or openai-compatible. Written for you by rupu auth login --account <name> --kind <vendor>. An unknown kind is a load error.
base_urlstringvendor URLRead by openai-compatible accounts.
default_modelstringnoneBeats the global default_model for agents on this account.
streambooltruefalse forces non-streaming for servers without SSE.
org_idstringnoneOpenAI only; sent as OpenAI-Organization.
regionstringnoneAccepted; not used by any shipped client today.
timeout_msinteger1200000 counts as unset.
max_retriesinteger1Retries after the first attempt on a retryable error.
max_concurrencyintegeranthropic 4, openai 8, gemini 4, copilot 4In-flight requests per account; 0 counts as unset.
prompt_cachebooltrueAnthropic only. false for gateways that reject cache_control; an agent's anthropicPromptCache overrides it.
modelsarray of tables[]Each entry: id (required), context_window and max_output (optional; when unset or 0 the limit is discovered from the provider's model list).
[providers.anthropic]
default_model = "claude-sonnet-4-6"
max_concurrency = 6

[providers.lab-llm]              # an OpenAI-compatible endpoint
kind = "openai-compatible"
base_url = "http://10.0.0.20:8000/v1"
default_model = "qwen3-coder"
models = [{ id = "qwen3-coder", context_window = 131072, max_output = 8192 }]

An openai-compatible account must set base_url and default_model, and may not reuse a reserved name (anthropic, openai, gemini, copilot, local, github, gitlab, linear, jira).

SCM and trackers — [scm.*], [[scm.rules]], [issues]

KeyNotes
[scm.default].platformFallback platform when a tool call omits platform. (owner / repo here are deprecated and never read.)
[issues.default].trackerFallback tracker when a tool call omits tracker.
[issues.default].project<owner>/<repo> default for rupu issues list, between an explicit --repo and git-remote autodetection. Needs tracker set too.
[scm.<account>]One table per SCM account ([scm.github], [scm.gh-work]). Keys: kind (github | gitlab; omit when the name is the platform), base_url, timeout_ms (default 30000), max_concurrency (github 8, gitlab 6), clone_protocol (https | ssh, default https), and GitLab-only oauth_client_id — the OAuth application SSO logs in as (glab's on gitlab.com; required for self-managed; read from the global file only).
[[scm.rules]]Account selection when two accounts share a platform. Each entry sets account plus exactly one of owner (repo-owner glob) or path (cwd glob). Precedence: --account → owner rule → path rule → the sole account of that platform → error. rupu scm bind appends one.
[scm.github]
clone_protocol = "ssh"

[scm.gh-work]
kind = "github"
base_url = "https://ghe.example.com/api/v3"

[[scm.rules]]
owner = "example-corp/*"
account = "gh-work"

[[scm.rules]]
path = "~/Code/work/*"
account = "gh-work"

See Integrations for the full routing story.

Terminal UI — [ui]

KeyTypeDefaultNotes
colorstringautoauto | always | never. NO_COLOR and --no-color force never.
themestringnoneOne name for both syntax and palette: a palette theme name selects that palette (and its paired syntax theme); a syntax theme name selects that syntax theme.
live_viewstringfocusedfocused | compact | full — density of the workflow live view.
pagerstringautoauto | always | never.
editorstring$VISUAL / $EDITORCommand used by agent edit/create and workflow edit/create.
[ui.syntax].themestringbase16-ocean.darkCode-highlighting theme; beats theme. Built-ins include base16-ocean.dark, base16-eighties.dark, base16-mocha.dark, base16-ocean.light, InspiredGitHub, Solarized (dark), Solarized (light).
[ui.palette].themestringrupu-darkCLI status colors; beats theme. Built-ins: rupu-dark, rupu-light, rupu-midnight, tokyo-night, dracula, gruvbox-dark, github-dark, github-light.
[ui.cp].shellstringv1v1 | v2 — control-plane web shell. Read from the global file on every page load, so a browser refresh switches it.

Custom themes live in ~/.rupu/themes/ or a project's .rupu/themes/. rupu ui themes lists what's available (marking the current one); rupu ui theme show|validate|import manages them, and import accepts native rupu theme files and Base16 schemes.

[ui]
live_view = "compact"
pager = "never"

[ui.palette]
theme = "tokyo-night"

[ui.syntax]
theme = "base16-mocha.dark"

The bash tool — [bash]

KeyTypeDefaultNotes
timeout_secsinteger120Timeout for one bash tool invocation.
env_allowlistarray<string>[]Extra env vars forwarded into the subprocess, beyond the always-allowed PATH, HOME, USER, TERM, LANG (and RUPU_RUN_ID, the run marker a launched child carries).

Machine limits — [runtime]

KeyTypeDefaultNotes
max_open_filesintegerunsetSoft open-file limit rupu raises itself to at startup, and the cap for automatic growth under fan-out. Unset: raise to 10240 (capped at the hard limit) and grow on demand. Past the cap, new agent runs are paced instead of failing with "Too many open files". Env: RUPU_MAX_OPEN_FILES / --max-open-files.
max_concurrent_jobsintegerRAM-derivedProcess-wide ceiling on local agent jobs, whatever a workflow's max_parallel: / max_concurrency: says. Unset: clamp(RAM_GB / 4, 4, 128). 0 means no ceiling. Remote (distribute:) units are bounded by their own host. Env: RUPU_MAX_CONCURRENT_JOBS.
min_free_memory_mbintegerRAM-derivedMemory watchdog: a new local job waits until this much memory is free (proceeding anyway after 60 s, with a warning). Unset: max(RAM × 10%, 4096). 0 disables the watchdog (the ceiling still applies). Available memory is read from /proc/meminfo (Linux) or vm_stat (macOS); where it can't be read, the watchdog does nothing. Env: RUPU_MIN_FREE_MEMORY_MB.

Child runs — [dispatch]

Child runs an agent starts with dispatch (see Agents).

KeyTypeDefaultNotes
max_concurrent_childreninteger8How many children of one parent run run at once, all kinds together. More wait as pending until a slot frees. 0 is read as 1. Applies per run, so an agentiflow's units count against its lead's limit. In-process sub-agents also wait for [runtime] admission.

Message spaces — [messaging]

A run's message space (see Messaging).

KeyTypeDefaultNotes
log_max_mbinteger64Size past which a space's log.jsonl takes no more messages. A further send fails to its sender with an error and a messaging_full notice. The log is never trimmed. Receipts are still written.

Workflow run: steps — [workflow]

KeyTypeDefaultNotes
run_step_enabledboolfalseWhether run: steps (which execute declared commands) may run. A workflow that reaches one while this is off fails; it never silently skips it.
run_step_allowlistarray<string>[]Executables a run: step may invoke, matched on basename (/usr/bin/python3 and python3 gate alike). Empty means any.
[workflow]
run_step_enabled = true
run_step_allowlist = ["python3", "make"]

Recovery — [recovery]

What happens when a model's reply can't be used or the provider fails the request. Background: Recovery & reliability.

KeyTypeDefaultNotes
fallbacksarray of { model, provider? }[]Ordered fallback chain for every agent that declares no fallbacks: of its own (an agent's list replaces this one, not merged). An entry without provider means the provider the run started on; entries on that provider are tried first, then other providers. A cross-provider entry sends the conversation to that provider's API. Unknown keys are an error.
server_side_fallbackbooltrueAsk Anthropic to fall back server-side when the requested model refuses. Applies only to API-key auth on claude-fable-5, claude-fable-5-1, claude-opus-5, claude-opus-5-5 and claude-sonnet-5-5; never OAuth. Set false to stop sending it.
[recovery]
fallbacks = [
  { model = "claude-sonnet-5-5" },
  { provider = "openai-codex", model = "gpt-5.6-cyber" },
]
server_side_fallback = true

Findings — [findings]

KeyTypeDefaultNotes
artifact_max_bytesinteger500 MiBPer-file cap for copying an artifact into the store; larger files are recorded by hash only.
artifact_max_filesinteger500Most files one report's artifacts may expand to (a directory counts every file inside).
artifact_total_max_bytesinteger2 GiBMost bytes one report's copied artifacts may add up to.
report_max_bytesinteger256 KiBSize budget for one full finding report.
ticket_patternsarray<string>[]Regexes or URL prefixes identifying your existing tickets; added to the finding-writing guidance agents get.
export_id_prefixstringSECPrefix of exported report numbers (SEC-001). Global file only; must match ^[A-Za-z][A-Za-z0-9_-]{0,15}$.

rupu findings import reads the size limits from the global file only. See Findings.

Concern catalogs — [coverage]

How a run's concern catalog is rendered into its system prompt; see Coverage for render modes.

KeyTypeDefaultNotes
full_mode_max_concernsinteger80Most concerns a run's flattened catalog may hold for its mode: auto includes to render in full; a larger catalog renders them as the one-line index the agent searches with coverage.concerns.search / coverage.concerns.detail. Includes that say mode: full or mode: index keep their mode.

Network capture — [netflow]

KeyTypeDefaultNotes
subprocess_capturebooltrueMaster switch for recording the connections an agent's bash commands open, by passively observing the OS socket table (no proxy, no root). Where the backend can't start, the run records why. RUPU_NETFLOW_SUBPROCESS=0 forces it off.
subprocess_poll_msinteger50Linux only: interval between socket-table polls while a bash call runs.
subprocess_linger_msinteger3000How long after the shell exits its sockets are still attributed to that call. A socket still open at the end is recorded as still open. Clamped to one day.
asn_auto_refreshbooltrueKeep the IP → ASN enrichment table fresh: the control plane downloads it in the background when it is missing or stale.
asn_refresh_interval_daysinteger7Age after which the ASN table is re-downloaded. 0 treats it as always stale.
asn_source_urlstringhttps://iptoasn.com/data/ip2asn-combined.tsv.gzWhere the ASN table comes from (gzipped ip2asn TSV) — point it at a mirror on an air-gapped network.
cp_index_budget_mbinteger256Memory budget for the flow rows rupu cp serve keeps indexed. Over budget, the oldest files' rows are dropped and re-read from disk on demand — slower, never different; per-file summaries always stay in memory. Status: GET /api/netflow/index. Takes effect on the next request after a change made through the control plane; a hand edit needs a restart.
[netflow]
asn_source_url = "https://mirror.internal.example/ip2asn-combined.tsv.gz"
asn_refresh_interval_days = 30
subprocess_linger_ms = 5000

See Netflow.

Control plane — [cp]

Background loops rupu cp serve runs in-process, so autoflows, triggers and gate timeouts work with no external cron.

The loop toggles and intervals below, like agentiflow.reaper_enabled and netflow.asn_auto_refresh, are read once when cp serve starts: change them and restart it. Saving the global config from the control plane lists the ones you changed as restart_required; every other key applies at once.

KeyTypeDefaultNotes
max_workspace_bytesinteger256 MiBLargest workspace-sync payload or delta for placed runs.
autoflow_reconcile_enabledbooltrueRun the autoflow reconcile loop (issue/PR entity autoflows).
autoflow_reconcile_interval_secsinteger60
cron_tick_enabledbooltrueRun the cron / polled-event trigger tick (what rupu cron tick does).
cron_tick_interval_secsinteger60
gate_sweep_enabledbooltrueEnforce gate on_timeout routing and reap orphaned runs whose runner process died. The same tick also drives the ASN refresh and the agentiflow reaper, each gated by its own key.
gate_sweep_interval_secsinteger60
Retired UI flags. agent_authoring_ui and workflow_editor_ui are no longer read: the agent builder and workflow editor are the only UIs. They still parse (with a warning) so an old config keeps loading — delete them.

Agentiflow supervision — [agentiflow]

KeyTypeDefaultNotes
serve_enabledbooltruefalse makes rupu agentiflow serve say it's disabled and exit.
serve_interval_secsinteger60Seconds between agentiflow serve sweeps (cp serve reaps on its gate-sweep tick).
reaper_enabledbooltrueFinalize a run whose coordinator died as failed and stop its units — in both agentiflow serve and cp serve.

See Agentiflows.

Event triggers — [triggers]

KeyTypeDefaultNotes
poll_sourcesarray of string | table[]Sources polled for event-triggered workflows: github:owner/repo, gitlab:group/project, linear:<team-id>, jira:<site>/<project>. A table entry adds poll_interval ("5m") and account — the only way to choose between two tracker accounts of one kind.
max_events_per_tickinteger50Cap on events processed per source per tick.
[triggers]
poll_sources = [
  "github:example-org/api",
  { source = "linear:team-1234", poll_interval = "10m", account = "linear-ops" },
]

See Triggers.

Autoflow defaults — [autoflow]

KeyTypeDefaultNotes
enabledboolfalse
repostringnonee.g. github:example-org/api.
checkoutstringworktreeworktree | in_place.
worktree_rootstringnoneOnly with checkout = "worktree".
permission_modestringnoneOnly bypass or readonly; ask is rejected for unattended runs.
strict_templatesboolfalse
max_activeintegerunboundedCap on concurrently active claims.
cleanup_afterstringnevere.g. 7d — completed/released claims and their worktrees are pruned after this.

See Autoflows.

Pricing — [pricing]

Turns token counts into USD for rupu usage, run listings and the control plane. Lookup order: [pricing.<provider>."<model>"] override → built-in table (each vendor's standard-tier list rate) → [pricing.agents.<agent>]. A named account prices through its kind, and an account-level override beats the vendor one. Model ids match exactly, then with a trailing [tag] stripped, then with a date snapshot stripped.

FieldTypeRequiredNotes
input_per_mtokfloatyesUSD per million input tokens.
output_per_mtokfloatyesUSD per million output tokens (reasoning tokens included).
cached_input_per_mtokfloatnoCache reads; defaults to the input rate.
cache_write_per_mtokfloatnoCache writes; defaults to the input rate.
[pricing.anthropic."claude-sonnet-4-6"]
input_per_mtok = 3.0
output_per_mtok = 15.0
cached_input_per_mtok = 0.30
cache_write_per_mtok = 3.75

[pricing.lab-llm."qwen3-coder"]    # an openai-compatible account
input_per_mtok = 0.4
output_per_mtok = 1.2

[pricing.agents.triage-bot]        # fallback when no model price is known
input_per_mtok = 0.5
output_per_mtok = 1.5

Retention — [storage]

KeyTypeDefaultNotes
archived_session_retentionstring30dDefault cutoff for rupu session prune.
archived_transcript_retentionstring30dDefault cutoff for rupu transcript prune / rupu cleanup.

Updates — [update]

KeyTypeDefaultNotes
channelstringstablestable | beta — which channel rupu update follows.
checkbooltruePrint a passive "update available" notice on normal commands.

Deprecated keys

These still parse — so an old file doesn't lose every other setting — but drive nothing. Loading one logs a warning; delete them, as a future release will reject them.

KeyReplacement
[retry] (max_attempts, initial_delay_ms)[providers.<name>].max_retries
[cp].agent_authoring_uinone — the agent builder is the only UI
[cp].workflow_editor_uinone — the workflow editor is the only UI
[scm.default].owner, [scm.default].repopass --repo, or set [issues.default].project