Docs

Configuration

Four layers of settings, and the keys that actually matter.

Edit on GitHub (opens in a new tab)

Precedence

Every setting can arrive four ways. When two of them disagree, the higher one wins, and the order is fixed:

  1. Overrides saved from the UI, in the SQLite settings table.
  2. Environment variables.
  3. The config file at KARUCODE_CONFIG — default ~/.config/karucode/config.toml.
  4. Built-in defaults.

The first layer is the one that surprises people: a value you changed in the browser keeps beating the environment on the next start, because it is stored in the database, and the database outranks the environment.

KARUCODE_LOCK_SETTINGS=1

Setting this removes layer 1 entirely: the UI stops writing settings and browser attempts to change them are rejected with 403. The environment and the config file become the only sources, which is what you want when a deployment is managed by something else.

Environment variables

The table is the reference. Anything not listed here still has a config-file equivalent or a flag on the binary — --help lists the flags.

Variable Default Meaning
KARUCODE_HOST / KARUCODE_PORT 127.0.0.1 / 8787 bind address; a non-loopback bind needs a token or sign-in
KARUCODE_WORKSPACE cwd default workspace root
KARUCODE_DATA_DIR ~/.local/share/karucode SQLite database + plugin data, mode 0700
KARUCODE_AUTH_TOKEN unset bearer token for remote access (≥16 chars)
KARUCODE_ADMIN_TOKEN unset required to change approvals or the endpoint
KARUCODE_PROJECT_ROOTS unset (any dir) colon-separated allowlist for new project roots
KARUCODE_ALLOW_TEMP_DIR true let file tools also reach the system temp dir
KARUCODE_BASE_URL http://127.0.0.1:4000/v1 OpenAI-compatible base URL
KARUCODE_API_KEY unset provider key (falls back to OPENAI_API_KEY)
KARUCODE_MODEL gpt-4o-mini model id
KARUCODE_APPROVAL_MODE ask ask, ask_commands, auto, read_only
KARUCODE_ALLOW_COMMANDS empty command prefixes that are never prompted for
KARUCODE_MAX_ITERATIONS 0 turns per run; 0 = no ceiling
KARUCODE_MAX_RUN_SECS 86400 hard wall clock for a whole run
KARUCODE_PARALLEL_TOOL_CALLS true run independent tool calls at once
KARUCODE_COMMAND_TIMEOUT_SECS 120 default shell timeout
KARUCODE_MAX_TOOL_OUTPUT_BYTES 24000 tool result bytes kept for the model
KARUCODE_CONTEXT_MAX_CHARS 0 0 = derive from the model’s window
KARUCODE_SUBAGENT_CONCURRENCY 3 concurrent subagents (0 = no ceiling)
KARUCODE_SUBAGENT_MAX_BACKGROUND 8 live detached agents per session
KARUCODE_ALLOW_RECURSIVE_SUBAGENTS false nested delegation
KARUCODE_LOG info tracing filter, e.g. info,karucode=debug
KARUCODE_ALLOWED_HOSTS empty extra Host names for the loopback guard
KARUCODE_ACCESS_ENABLED false require a password or passkey to open the UI
KARUCODE_PUBLIC_URL unset the address people type; added to the Host allowlist
KARUCODE_SESSION_DAYS 30 how long a browser stays signed in
KARUCODE_TUNNEL none none, cloudflare or custom — which proxy is believed

Limits

A run is not cut off by a turn counter. max_iterations defaults to 0 — no ceiling — so a long task is not interrupted and no continuation turn is injected into the transcript to restart it:

[limits]
max_iterations = 0        # 0 = no turn ceiling (the default)
max_run_secs = 86400      # hard wall clock for the whole run, 24 h
parallel_tool_calls = true
  • max_iterations — turns allowed in one run. 0 means the run ends when the work is done, when you cancel it, at the wall clock, or when the loop guard catches it re-issuing calls with no workspace change. A non-zero value is a blunt backstop: the run stops with limit_reached and says so.
  • max_run_secs — a hard wall clock for the whole run. 86400 is 24 hours.
  • parallel_tool_calls — independent calls (reads, searches, disjoint writes) run at once. Anything that can see a sibling’s effects — shell, plugins, MCP, subagents, approvals — still runs in call order, and results are folded into the transcript in call order either way.

There is no segment and no continuation: a long task is one long task. The wall clock, the cancel button and the loop guard are what end a run.

Web-UI access

Sign-in is a password or a passkey, exchanged for an HttpOnly session cookie. The credentials are not configuration — the password hash and passkeys live in the SQLite settings table and are managed from Settings → Access or the first-run wizard — so [access] holds only the policy:

[access]
enabled = true                               # require sign-in; default false
public_url = "https://karucode.example.com"  # added to the Host allowlist
session_days = 30                            # 1..=365
tunnel = "cloudflare"                        # none | cloudflare | custom
trusted_proxies = ["10.0.0.0/8"]             # whose forwarded headers are believed

Creating the first credential turns sign-in on, so setting a password is the whole switch. enabled = true with no credential is refused: that would leave a login screen nobody can pass. There is deliberately no environment variable for the password — a hash in a unit file ends up in systemctl show. Security covers the session and passkey rules.

Providers

A provider is a name, a base URL and the key to send. Named profiles let you keep several and switch the active one without editing the environment:

active_provider = "gateway"
active_model = "gpt-4o-mini"

[[providers]]
id = "gateway"
name = "Local gateway"
provider = "openai"
base_url = "http://127.0.0.1:4000/v1"
api_key = "sk-..."
request_timeout_secs = 600

base_url is a prefix — karucode appends /chat/completions. Providers are install-wide, not per project; a session can pin its own provider, model and reasoning effort, and that pin travels with the session rather than the checkout.

Plugins and MCP

Both are separate processes. A plugin speaks versioned JSON-RPC over stdio; an MCP server speaks MCP over stdio or streamable HTTP. Declared, they look like this:

[[plugins]]
name = "background-terminals"
command = "kc-plugin-terminals"
args = ["--stdio"]

[[mcp_servers]]
name = "kg"
command = "kg-mcp"
args = ["--kb", "karucode"]
transport = "stdio"

Tools are namespaced so nothing can shadow a built-in: plugin__<name>__<tool> and mcp__<server>__<tool>. MCP surfaces tools only — resources and prompts are not exposed — and the legacy SSE-only transport is not implemented.

Neither kind is sandboxed. A plugin runs with your permissions. A rebuilt plugin is picked up by a reload rather than a restart: POST /api/plugins/reload rescans, POST /api/plugins/:name/reload replaces one process, and plugins_watch (off by default) does it on a timer. Read the code before you add it, and see Security.

Approvals

One variable picks the gate: KARUCODE_APPROVAL_MODE. It decides what the agent must ask for before it acts.

Mode What happens
ask Shell commands and file writes wait for you. The default.
ask_commands Shell commands wait; file writes do not.
auto Nothing waits. Only for a workspace you would let the model change unattended.
read_only No shell commands and no writes.

Two more keys change the edges of that gate. KARUCODE_ALLOW_COMMANDS is a list of command prefixes that are never prompted for — cargo test is the obvious entry. KARUCODE_ADMIN_TOKEN is required to change approvals or the endpoint at all, so a session token cannot widen its own permissions.

Approvals are a gate, not isolation: an approved command runs as the server user with no sandbox around it. auto mode is only safe for a workspace you would hand to the model directly.