Precedence
Every setting can arrive four ways. When two of them disagree, the higher one wins, and the order is fixed:
- Overrides saved from the UI, in the SQLite
settingstable. - Environment variables.
-
The config file at
KARUCODE_CONFIG— default~/.config/karucode/config.toml. - 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.0means 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 withlimit_reachedand says so. -
max_run_secs— a hard wall clock for the whole run.86400is 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.