Configuration
Keelson reads its settings from one file in your home directory and a handful of
KEELSON_* environment variables. The most common thing you will set is which
agent providers load and which one new chats default to.
The config file
Section titled “The config file”Settings live in config.json under the keelson home, which is ~/.keelson by
default (%LOCALAPPDATA%\keelson on Windows). It is plain JSON, optional, and
read at server boot. A missing file falls back to the defaults silently; a
malformed one is ignored with a warning, so a typo never blocks startup.
Only a broken shape warns, though: invalid JSON, or a key whose value is the
wrong type. An unrecognized top-level key is accepted and dropped without a
warning, so a misspelled key (crossRibGrant for crossRibGrants) reads as a
clean file whose setting silently never applies. Check spelling with keelson doctor, which reports what the server actually resolved rather than what the
file appears to say.
{ "providers": { "copilot": true, "claude": true }, "defaultProvider": "claude"}Enabling a provider is only half of it. Keelson ships with Copilot; the Claude, Codex, and pi SDKs are large and vendor-specific, so they install on demand:
keelson provider add claude # or: codex, pikeelson restartA provider that is enabled here but whose SDK is not installed is left
unregistered rather than offered and broken — keelson doctor reports it and
names the fix. keelson provider list shows what is bundled, installed, and
enabled.
| Key | Type | What it does |
|---|---|---|
providers | object of id: boolean | Turns each provider on or off. Merged over the defaults, so list only what you want to change. Claude, Codex, and pi also need keelson provider add <id>. |
defaultProvider | string | The provider new chats and the workflow fallback preselect, when it is enabled. Falls back when it is not. See The default provider. |
claude.auth | "auto" | "subscription" | "api-key" | How the Claude provider picks a credential. Default auto. See Claude credentials. |
copilot.modelClasses, claude.modelClasses, codex.modelClasses, pi.modelClasses | object | Optional fast, balanced, and deep model-id overrides for portable workflow tiers. See Model classes. |
codex.sandbox | "read-only" | "workspace-write" | "danger-full-access" | How much the Codex subprocess may touch. Default workspace-write. See Codex. |
codex.network | boolean | Whether the Codex subprocess may reach the network. Default false. |
mcp | object | Tunes the MCP gateway, the /api/mcp endpoint other agents call. See MCP gateway. |
crossRibGrants | object of caller: { target: [tool] } | Which ribs may call which other ribs’ tools. Default-deny without an entry. See Cross-rib grants. |
ribWorkflowGrants | object of rib: [workflow] | Which ribs may start which catalog workflows. Default-deny without an entry. See Rib workflow grants. |
ribApprovalGrants | object of rib: [workflow] | Which ribs may answer the approval gates of which workflows, on runs they started. Default-deny without an entry. See Rib approval grants. |
gateways | array | OpenAI-compatible endpoints registered as providers. Managed with keelson gateway, not by hand. See Gateways. |
modelPrices | object of model: { rates } | USD-per-million-token rates the Usage ledger prices turns with, consulted before the bundled Anthropic table. See Model prices. |
The built-in provider ids are copilot, claude, pi, codex, and stub (the
offline echo provider). Out of the box, only copilot loads; stub, claude, pi,
and codex are off, so a fresh install defaults to Copilot. Flip the ones you
want:
// Claude only{ "providers": { "copilot": false, "claude": true }}Precedence
Section titled “Precedence”A provider is enabled by the first of these that has an opinion:
KEELSON_PROVIDERS, a comma-separated list, when set. It is an exact override: only the providers you name load, ignoring the config file.- The
providersmap inconfig.json. - The built-in defaults (copilot on; stub, claude, pi, and codex off).
So KEELSON_PROVIDERS=stub gives you an offline run with no keys regardless of
what config.json says, and the config file is the durable setting for everyday
use.
The default provider
Section titled “The default provider”defaultProvider is a preference, not a guarantee: it is honored only when that
provider actually registered. Resolution walks a chain, taking the first that
holds:
defaultProvider, when it is registered and is notworkflow. That id names an internal meta-provider, not an agent, so it is rejected as a default.copilot, if registered.- The first registered provider that is neither
stubnorworkflow. stub, if registered.
So a defaultProvider naming a provider you never enabled, or misspelled, does
not error. It falls through the chain to whatever is loaded. keelson doctor
prints the resolved default and where it came from, which is how you catch the
difference between the default you asked for and the one you got.
Model classes
Section titled “Model classes”Portable workflow models fast, balanced, and deep select a provider’s
model-class mapping. Copilot derives its mapping from the account’s live catalog;
other providers expose their own classes or a default. To pin a class, set a
model id inside that provider’s top-level settings object, not inside the boolean
providers map:
{ "providers": { "copilot": true, "claude": true }, "copilot": { "modelClasses": { "deep": "a-model-available-to-your-account" } }, "claude": { "modelClasses": { "fast": "claude-haiku-4-5" } }}An explicit model on the turn, or model_by_provider on a workflow node, takes
precedence. Otherwise each configured class id wins over that provider’s derived
class; omitted keys keep the derived mapping. The provider default is the final
fallback. Copilot’s default is always auto, including unclassed turns and
explicit auto selections. With no concrete catalog available, each unpinned
Copilot class also requests auto. A single-model catalog can legitimately map
all three classes to one id.
Once Copilot derives concrete classes, the class map stays fixed for the server
process. Until then, a later catalog fetch can still fill it in. Restart the
server after changing config.json or to derive classes from a newer Copilot
catalog; a
server-down CLI invocation loads the current config anew. keelson doctor warns
when the three effective classes request the same id, including auto or a
single-model gateway. --strict makes that warning a failing exit status. Set
gateway overrides in that gateway’s modelClasses field under
gateways, not in a root provider block.
Claude credentials
Section titled “Claude credentials”The claude provider runs the Claude Agent SDK, which can authenticate with a
Pro/Max subscription (your claude CLI login) or an API key
(ANTHROPIC_API_KEY). The SDK prefers an explicit API key whenever one is in the
environment, so keelson lets you choose with claude.auth:
{ "providers": { "claude": true }, "claude": { "auth": "auto", "modelClasses": { "fast": "claude-haiku-4-5", "balanced": "claude-opus-4-8", "deep": "claude-fable-5" } }}| Value | Behavior |
|---|---|
auto (default) | Use the subscription when claude auth status reports one, otherwise fall back to the API key. |
subscription | Always use the subscription login; the turn errors if there is none. |
api-key | Always use the API key (a keelson-saved token, otherwise ANTHROPIC_API_KEY). |
To reach the subscription, keelson removes ANTHROPIC_API_KEY from just the
Claude process it spawns. Your shell keeps the variable for every other tool, so
you never unset it globally.
claude.modelClasses follows the model-class precedence. Put
it beside auth, not inside the boolean providers enablement map.
Pi (community provider)
Section titled “Pi (community provider)”pi wraps the pi coding agent,
a multi-vendor agent that can reach Anthropic, Google, OpenAI, and more behind one
interface. It is opt-in:
{ "providers": { "pi": true }, "pi": { "modelClasses": { "fast": "anthropic/claude-haiku-4.5", "balanced": "anthropic/claude-sonnet-4.6", "deep": "anthropic/claude-opus-4.5" } }}Pi manages its own credentials, so there is no keelson sign-in for it. Pi reads
~/.pi/agent/auth.json (set up with pi’s own login) and per-vendor API-key
environment variables such as ANTHROPIC_API_KEY or GEMINI_API_KEY. Because pi
is multi-vendor, models are named vendor/model (for example
anthropic/claude-opus-4.5 or google/gemini-2.5-pro); pick one your credentials
cover in the model picker, or leave the default and let pi choose from its own
settings. Use pi.modelClasses to pin any portable workflow tier to a model your
configured vendor credentials can reach.
Codex (community provider)
Section titled “Codex (community provider)”codex wraps OpenAI’s Codex SDK,
which drives the native codex CLI. It is opt-in:
{ "providers": { "codex": true }}Like pi, Codex manages its own credentials, so there is no keelson sign-in for
it. Codex reads ~/.codex/auth.json (set up with codex login) or an
OPENAI_API_KEY / CODEX_API_KEY environment variable. Run keelson doctor to
confirm Codex found a credential.
Codex is an agentic coding provider: its turns run inside the codex exec
subprocess, which reads the repo and runs its own shell and file-edit tools. Those
tool calls execute under Codex’s own sandbox, which keelson cannot gate per call
the way it gates Copilot and Claude tools, so the sandbox is the boundary. By
default keelson runs Codex with sandbox: "workspace-write" (read the repo, write
within the conversation’s project directory), approvalPolicy: never, and network
access off. Tighten or loosen it with codex.sandbox, and toggle network reach
with codex.network:
{ "providers": { "codex": true }, "codex": { "sandbox": "read-only", "modelClasses": { "fast": "gpt-6-luna", "balanced": "gpt-6-luna", "deep": "gpt-6-sol" } }}codex.modelClasses overrides the catalog-derived workflow tiers. It sits beside
sandbox and network in the top-level codex settings object.
| Value | Behavior |
|---|---|
read-only | Codex may read the repo and reason, but not write files or run mutating commands. |
workspace-write (default) | Codex may also write within the project directory. |
danger-full-access | No sandbox restrictions. |
Codex resumes conversations across turns (it persists threads under
~/.codex/sessions), and its command, file-change, and reasoning activity surface
in chat as tool and thinking rows. Leave the model unset to use your ~/.codex
default, or pick a gpt-5.x model in the picker.
Gateways
Section titled “Gateways”A gateway points keelson at any OpenAI-compatible endpoint (OpenRouter, a local Ollama or vLLM, Azure OpenAI, or a LiteLLM proxy) and registers it as a provider named for the gateway. It then shows up in the model picker like any built-in. Manage gateways with the CLI, which writes the config and stores the key for you:
keelson gateway add ollama http://localhost:11434/v1 --model qwen3keelson gateway add openrouter https://openrouter.ai/api/v1 --model openai/gpt-4o --key sk-...keelson gateway listkeelson gateway remove ollamaThe base URL and default model persist to config.json under gateways; the API
key, when the endpoint needs one, goes to your OS keychain, never the config file.
A local Ollama needs no key, so omit --key for a keyless endpoint. --key also
reads KEELSON_GATEWAY_KEY, so the key stays out of shell history. The same
operations are available over the API at /api/gateways.
Keyless endpoints may use remote HTTP. A gateway with a stored key must use HTTPS
or HTTP on localhost, 127.0.0.1, or ::1. If a stored key would cross remote
plain HTTP, model discovery makes no request, reports a server warning, and
remains not checked. The picker can still use the configured model fallback.
Chat makes no request and returns an explicit error directing you to HTTPS or a
loopback host.
// ~/.keelson/config.json (written by `keelson gateway add`){ "gateways": [ { "name": "ollama", "baseUrl": "http://localhost:11434/v1", "protocol": "openai", "model": "qwen3", "modelClasses": { "fast": "qwen3", "balanced": "qwen3", "deep": "qwen3" } } ]}A gateway name must be lowercase kebab-case and cannot shadow a built-in provider
id (copilot, claude, pi, codex, stub). The only wire protocol today is
openai. Gateway turns stream, but keelson does not yet project its tool registry
into them (tools: false) and they do not resume a session, so each turn is
fresh and tool-less. Each gateway entry may set its own modelClasses map. See
the provider reference for where
gateways sit in the matrix.
Model prices
Section titled “Model prices”The Usage tab prices each turn from a per-model rate
table. Keelson bundles the Anthropic list prices for the Claude models its Claude
provider offers and recognizes them under the Copilot catalog’s dotted ids too.
Copilot models also carry per-token rates in the account’s live catalog, and
those price turns once the catalog has loaded (base-context rates; the ledger does
not record which turns crossed into long context). Every other model is unpriced
until you add it under modelPrices, keyed by the
model id the ledger records (the served model, so the id a provider reports, not
a class alias), with all four rates in US dollars per million tokens:
{ "modelPrices": { "my-gateway-model": { "inputPerMTok": 1, "outputPerMTok": 4, "cacheReadPerMTok": 0.1, "cacheWritePerMTok": 0 }, "claude-opus-4-8": { "inputPerMTok": 4.5, "outputPerMTok": 22.5, "cacheReadPerMTok": 0.45, "cacheWritePerMTok": 5.6 } }}An entry overrides a Copilot catalog rate or a bundled rate, and the id matches
whether you write it dotted or hyphenated. All four rates are required: an entry
missing one fails the config read rather than pricing that dimension at zero.
Cost is computed when a Usage view is read, so an edit here reprices history on
the next load with no restart. A model with no entry shows as unpriced, never
as a zero cost.
Governance
Section titled “Governance”The policy engine gates tool calls. Three opt-in builtins cover the common cases:
KEELSON_ASK_ON_SHELL=1 pauses for approval before a shell or file-mutating call,
KEELSON_TURN_BUDGET / KEELSON_COST_BUDGET cap a session’s spend, and
KEELSON_REDACT_PATTERN scrubs secrets from tool output. Each is one environment
variable; the Governance guide covers what they do and how to
resolve an approval.
Cross-rib grants
Section titled “Cross-rib grants”A rib cannot reach another rib’s tools by default. When one rib’s agent turn asks
for a tool a sibling rib owns, the harness refuses unless the operator has granted
that caller → target → tool triple. Grant it in the config file, under
crossRibGrants:
{ "crossRibGrants": { "swarm": { "beads": ["beads_ready", "beads_show", "beads_update", "beads_close"] } }}That reads as: the Swarm rib may call these four of the Beads rib’s tools, so a
swarm’s lead can read the ready queue and close a bead as its work lands. "*"
covers every tool the target owns. A caller with no entry
is denied, which is the default for every pair.
Keep standing grants here rather than in KEELSON_CROSS_RIB_GRANTS. The two
union, so either works, but an env-only grant lapses the moment the server starts
from a shell that never exported it, and the capability it enabled goes quiet
rather than failing loudly. The same grant gates both a rib’s callTool and the
tools projected onto a rib’s agent turn, so a swarm lead handed the tracker’s
tools needs this to reach them.
Grants are matched by exact string, and a grant that matches nothing is silently
inert: a misspelled rib id, or a tool name the target renamed since you wrote the
grant, parses and stores just fine and then never fires. keelson doctor is where
you see this. Its ribs section reads the grants back from the running server,
lists the ones that server is enforcing, and warns when one is inert, naming the
rib or the tool that does not exist.
The server resolves grants once, at startup. Doctor reports what that server is actually holding rather than what your files say a fresh one would hold, so it catches the two ways the two drift apart. A grant you added to the config file but have not restarted into is denied right now, and doctor tells you to restart. A grant the running server holds that the config file does not name works right now, and a restart reproduces it only if the environment supplies it again, so doctor tells you to write it into the config file if you want it held regardless. Doctor will not tell you which source that grant came from, and will not promise that a restart revokes it: the server records only what it resolved, never where each grant originated, and a restart inherits the shell it starts from. A config file that fails to parse is reported as unreadable rather than read as granting nothing. All of this needs the server up: with it down there are no grants in force to observe, so doctor skips the section rather than guessing.
Rib workflow grants
Section titled “Rib workflow grants”A rib cannot start a catalog workflow by default. When a rib calls
startWorkflow, the harness refuses unless the operator has granted that rib
that workflow name. Grant it in the config file, under ribWorkflowGrants:
{ "ribWorkflowGrants": { "chat": ["fix-issue", "resolve-pr"] }}That reads as: the Chat rib may start fix-issue and resolve-pr. "*" covers
every catalog workflow; list names to keep it narrow. The grant is checked before
the policy engine, so a rib with no entry gets a plain denial and the operator is
never prompted. A granted start still passes through policy as a workflow_run
tool call on the rib surface. If a policy asks you to approve it, the rib waits
up to KEELSON_CROSS_RIB_CALL_TIMEOUT_MS, and no answer counts as a denial.
The grant names a workflow, not a file. When the rib starts a workflow on a
project, the name resolves in that project’s scope, so a fix-issue.yaml under
the project’s .keelson/workflows/ runs in place of the bundled one.
KEELSON_RIB_WORKFLOW_GRANTS unions with this key, and the same advice applies:
keep a standing grant in the config file. A granted rib can start a run, read the
status of runs it started, and cancel them. Answering the run’s approvals takes
the separate grant below.
Rib approval grants
Section titled “Rib approval grants”A run that stops at an approval gate waits for the operator. To let a rib answer
those gates on the runs it started, grant it the workflow names under
ribApprovalGrants:
{ "ribApprovalGrants": { "chat": ["fix-issue"] }}That reads as: the Chat rib may answer the approval gates of the fix-issue runs
it started, which means approving a plan or sending feedback on it, as
workflow_respond would. The grant is separate from the start grant, so a rib
allowed to start a workflow still leaves its gates to you until you add this one.
It never reaches runs the rib did not start. Like the start grant it is checked
before the policy engine, and a granted answer still passes policy as a
workflow_respond call on the rib surface. KEELSON_RIB_APPROVAL_GRANTS unions
with this key.
Grant only workflows whose gates a rib can judge from what it can read. A gate that guards a deploy or a publish is usually one to keep for yourself.
MCP gateway
Section titled “MCP gateway”The server exposes its tool registry over the Model Context Protocol
at /api/mcp, so another agent can call your ribs’ tools. It comes up with the
server, tokenless on loopback and exposing its full tool registry (state-changing
tools included) by default. The mcp block tunes it; Using keelson over
MCP is the task guide.
{ "mcp": { "enabled": true, "exposeStateChanging": true, "requireToken": false, "toolDenylist": [] }}| Key | Type | What it does |
|---|---|---|
enabled | boolean | Whether /api/mcp mounts. Default true. |
exposeStateChanging | boolean | Whether state-changing tools (workflow_run, workflow_respond, a rib’s mutating tools) cross. Default true; set false to restrict the endpoint to read-only tools. |
requireToken | boolean | Gate the endpoint behind the bearer token in server.json. Default false (loopback, no token). |
toolDenylist | string array | Tool names never exposed over MCP, whatever the flags above allow. |
Each key has a KEELSON_MCP_* environment override (see below) that wins over
the config value, the same way KEELSON_PROVIDERS wins over providers.
Environment variables
Section titled “Environment variables”The config file covers providers; these variables cover the rest and override
the matching config value when set. KEELSON_CROSS_RIB_GRANTS is the one
exception: it unions with the config file’s crossRibGrants rather than
replacing it, so a grant from either source stands.
| Variable | Default | What it controls |
|---|---|---|
KEELSON_HOME | ~/.keelson (%LOCALAPPDATA%\keelson on Windows) | The keelson home directory (db, workflows, config). |
KEELSON_SERVER_URL | http://127.0.0.1:7878 | URL the CLI probes when no --base-url is given. Set this to reach a server on a non-default host or port without repeating --base-url on every command. Must be a valid http or https URL; an invalid value throws immediately so the misconfiguration surfaces at once rather than appearing as “server down”. |
KEELSON_DB | <home>/keelson.db | The SQLite database file. keelson start --db overrides it per run. |
KEELSON_CONFIG | <home>/config.json | Path to the config file. |
KEELSON_WORKFLOWS_DIR | <home>/workflows | The global workflows directory. |
KEELSON_PROVIDERS | unset | Exact list of providers to load. Overrides the config file. |
KEELSON_WORKFLOW_PROVIDER | first real provider | Provider that backs workflow prompt nodes. |
KEELSON_WORKFLOW_TOOL_DENYLIST | unset | Operator floor: tool names no workflow prompt node may use, subtracted on top of whatever a node allows. |
KEELSON_APPROVAL_REVIEWER | unset | Operator floor: set to off to disable every approval-node reviewer, so all gates pause for a human. See Workflow nodes. |
KEELSON_ASK_ON_SHELL | unset | Set to 1 to pause for human approval before any shell or file-mutating call. See Governance. |
KEELSON_TURN_BUDGET | unset | Positive integer: cap a session’s model-calling turns. A downgrade gate, not a wall. See Budgets. |
KEELSON_COST_BUDGET | unset | Positive integer: cap a session’s accumulated input+output tokens. Same downgrade-gate behavior. |
KEELSON_REDACT_PATTERN | unset | Regex whose matches become [REDACTED] in tool results and workflow node output. See Redaction. |
KEELSON_GATEWAY_KEY | unset | API key keelson gateway add reads when --key is omitted, so it stays out of shell history. See Gateways. |
KEELSON_WORKFLOW_PROMPT_TIMEOUT_S | 600 | Per-node timeout, in seconds, for workflow prompt nodes. |
KEELSON_MAX_CONCURRENT_RUNS | 4 | Process-wide ceiling on worktree-isolated workflow runs executing at once; further runs queue. Raise on a larger host. |
KEELSON_CROSS_RIB_GRANTS | unset | ;-separated caller:target:tool grant triples (the tool field takes a comma-separated list or *); default-deny when unset. Unions with (does not override) the config file’s crossRibGrants, which is the durable place for a standing grant. See Cross-rib grants. |
KEELSON_RIB_WORKFLOW_GRANTS | unset | ;-separated rib:workflow grants (the workflow field takes a comma-separated list or *); default-deny when unset. Unions with the config file’s ribWorkflowGrants, like the cross-rib variable above. See Rib workflow grants. |
KEELSON_RIB_APPROVAL_GRANTS | unset | The same grammar, for the workflows whose approval gates a rib may answer on runs it started; default-deny when unset. Unions with the config file’s ribApprovalGrants. See Rib approval grants. |
KEELSON_CROSS_RIB_CALL_TIMEOUT_MS | 30000 | Timeout, in milliseconds, for one cross-rib tool call. See Governance. |
KEELSON_COPILOT_WARM_IDLE_MS | 600000 | How long the Copilot language-server subprocess stays warm between turns before it is evicted, in milliseconds. 0 or negative disables warmth (every turn spawns fresh). |
KEELSON_RIBS | all discovered | Comma-separated rib ids to activate. |
KEELSON_MCP_DISABLED | unset | Set to 1 to not mount /api/mcp. Overrides mcp.enabled. |
KEELSON_MCP_EXPOSE_STATE_CHANGING | unset | Set to 1 to force state-changing tools on, 0 to force read-only. Overrides mcp.exposeStateChanging (which defaults on). |
KEELSON_MCP_REQUIRE_TOKEN | unset | Set to 1 to gate /api/mcp behind the server.json token. Overrides mcp.requireToken. |
KEELSON_MCP_DENYLIST | unset | Comma-separated tool names to hide over MCP. Merged with mcp.toolDenylist. |
KEELSON_WORKSPACE | ~/keelson | Root for project workspaces. |
KEELSON_DISABLE_MUTATION_LOCK | unset | Emergency operator bypass: set to a non-empty value other than 0 / false to disable per-project mutation-lock enforcement (concurrent runs may then mutate the same live checkout). Leave unset in normal operation. |
KEELSON_BASH | auto-detected | Path to a POSIX shell for bash workflow nodes. Honored first on every platform; Git Bash autodetection is the Windows-only fallback, then bash from PATH. |
KEELSON_FORGE | unset | The forge selection the bash and loop node shim resolves. |
KEELSON_DISABLE_SCHEDULER | unset | Set to 1 to stop cadence and producer schedules from starting. |
Confirm what loaded
Section titled “Confirm what loaded”keelson doctor reports the resolved set under the auth category, so you can
check the config took effect:
auth ok keyring round-trip service=keelson ok providers enabled=stub, claude; default=claude (source: config.json)The source tells you where the decision came from: KEELSON_PROVIDERS,
config.json, or defaults.