Skip to content

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.

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.

~/.keelson/config.json
{
"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:

Terminal window
keelson provider add claude # or: codex, pi
keelson restart

A 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.

KeyTypeWhat it does
providersobject of id: booleanTurns 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>.
defaultProviderstringThe 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.modelClassesobjectOptional 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.networkbooleanWhether the Codex subprocess may reach the network. Default false.
mcpobjectTunes the MCP gateway, the /api/mcp endpoint other agents call. See MCP gateway.
crossRibGrantsobject of caller: { target: [tool] }Which ribs may call which other ribs’ tools. Default-deny without an entry. See Cross-rib grants.
ribWorkflowGrantsobject of rib: [workflow]Which ribs may start which catalog workflows. Default-deny without an entry. See Rib workflow grants.
ribApprovalGrantsobject 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.
gatewaysarrayOpenAI-compatible endpoints registered as providers. Managed with keelson gateway, not by hand. See Gateways.
modelPricesobject 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 }
}

A provider is enabled by the first of these that has an opinion:

  1. KEELSON_PROVIDERS, a comma-separated list, when set. It is an exact override: only the providers you name load, ignoring the config file.
  2. The providers map in config.json.
  3. 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.

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:

  1. defaultProvider, when it is registered and is not workflow. That id names an internal meta-provider, not an agent, so it is rejected as a default.
  2. copilot, if registered.
  3. The first registered provider that is neither stub nor workflow.
  4. 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.

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.

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"
}
}
}
ValueBehavior
auto (default)Use the subscription when claude auth status reports one, otherwise fall back to the API key.
subscriptionAlways use the subscription login; the turn errors if there is none.
api-keyAlways 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 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 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.

ValueBehavior
read-onlyCodex 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-accessNo 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.

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:

Terminal window
keelson gateway add ollama http://localhost:11434/v1 --model qwen3
keelson gateway add openrouter https://openrouter.ai/api/v1 --model openai/gpt-4o --key sk-...
keelson gateway list
keelson gateway remove ollama

The 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.

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.

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.

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.

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.

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.

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": []
}
}
KeyTypeWhat it does
enabledbooleanWhether /api/mcp mounts. Default true.
exposeStateChangingbooleanWhether 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.
requireTokenbooleanGate the endpoint behind the bearer token in server.json. Default false (loopback, no token).
toolDenyliststring arrayTool 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.

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.

VariableDefaultWhat it controls
KEELSON_HOME~/.keelson (%LOCALAPPDATA%\keelson on Windows)The keelson home directory (db, workflows, config).
KEELSON_SERVER_URLhttp://127.0.0.1:7878URL 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.dbThe SQLite database file. keelson start --db overrides it per run.
KEELSON_CONFIG<home>/config.jsonPath to the config file.
KEELSON_WORKFLOWS_DIR<home>/workflowsThe global workflows directory.
KEELSON_PROVIDERSunsetExact list of providers to load. Overrides the config file.
KEELSON_WORKFLOW_PROVIDERfirst real providerProvider that backs workflow prompt nodes.
KEELSON_WORKFLOW_TOOL_DENYLISTunsetOperator floor: tool names no workflow prompt node may use, subtracted on top of whatever a node allows.
KEELSON_APPROVAL_REVIEWERunsetOperator floor: set to off to disable every approval-node reviewer, so all gates pause for a human. See Workflow nodes.
KEELSON_ASK_ON_SHELLunsetSet to 1 to pause for human approval before any shell or file-mutating call. See Governance.
KEELSON_TURN_BUDGETunsetPositive integer: cap a session’s model-calling turns. A downgrade gate, not a wall. See Budgets.
KEELSON_COST_BUDGETunsetPositive integer: cap a session’s accumulated input+output tokens. Same downgrade-gate behavior.
KEELSON_REDACT_PATTERNunsetRegex whose matches become [REDACTED] in tool results and workflow node output. See Redaction.
KEELSON_GATEWAY_KEYunsetAPI key keelson gateway add reads when --key is omitted, so it stays out of shell history. See Gateways.
KEELSON_WORKFLOW_PROMPT_TIMEOUT_S600Per-node timeout, in seconds, for workflow prompt nodes.
KEELSON_MAX_CONCURRENT_RUNS4Process-wide ceiling on worktree-isolated workflow runs executing at once; further runs queue. Raise on a larger host.
KEELSON_CROSS_RIB_GRANTSunset;-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_GRANTSunset;-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_GRANTSunsetThe 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_MS30000Timeout, in milliseconds, for one cross-rib tool call. See Governance.
KEELSON_COPILOT_WARM_IDLE_MS600000How 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_RIBSall discoveredComma-separated rib ids to activate.
KEELSON_MCP_DISABLEDunsetSet to 1 to not mount /api/mcp. Overrides mcp.enabled.
KEELSON_MCP_EXPOSE_STATE_CHANGINGunsetSet to 1 to force state-changing tools on, 0 to force read-only. Overrides mcp.exposeStateChanging (which defaults on).
KEELSON_MCP_REQUIRE_TOKENunsetSet to 1 to gate /api/mcp behind the server.json token. Overrides mcp.requireToken.
KEELSON_MCP_DENYLISTunsetComma-separated tool names to hide over MCP. Merged with mcp.toolDenylist.
KEELSON_WORKSPACE~/keelsonRoot for project workspaces.
KEELSON_DISABLE_MUTATION_LOCKunsetEmergency 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_BASHauto-detectedPath 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_FORGEunsetThe forge selection the bash and loop node shim resolves.
KEELSON_DISABLE_SCHEDULERunsetSet to 1 to stop cadence and producer schedules from starting.

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.