Saphan StudioDocs
Configuration

Every key

Every configuration key that exists, by section, with its meaning and default.

The authority is saphan config set --help, and the error config set prints for an unknown key. Both list exactly what the binary in front of you accepts, and no page can be more current than the build it is describing. Where this page and your build disagree, your build decides.

⚠ This page describes the configuration FILE, which is one surface; config set writes most of it, but not every key. Sections that only a server reads at start-up are noted where they appear.

A credential's value is never a key. Every key that needs a secret names an entry in the one credentials file, or — for a few server keys — an environment variable. See Secrets.

How a key on this page becomes a line in the file

saphan config set takes one dotted key. The file has sections. The two spellings are related by one rule, and it is the same rule for every family below: the last dotted segment is the key; everything before it is the section header.

What you typeWhat the file carries
saphan config set lane.local.credential LITELLM_API_KEY[lane.local], then credential = "LITELLM_API_KEY" — the worked file under Configuration prints exactly that
saphan config set probe.model.claude-code "opus,fable"[probe.model], then claude-code = "opus,fable"
saphan config set notice.binding.person.sink slack[notice.binding.person], then sink = "slack"

That is why a repeated section below is headed [lane.<alias>] and [host.<id>] rather than [lane] and [host]: the variable segment belongs to the section name, not to the key.

⚠ So no key name in the tables below contains a dot. If you are reading a dotted name anywhere, you are reading a config set argument, and it splits at its last dot before it reaches the file. ⛔ Getting the split wrong can be silent. Many sections refuse an unknown key by name when the file loads, but some — [run] among them — skip a key they do not know, and a configuration that is quietly not read looks exactly like one that is. See Known limitations.

[identity]

KeyMeaning
actorwho is acting; the only key with the full flag/env/file ladder. Unset = act verbs refuse.
display_namename shown in interfaces; it identifies nothing
emailoptional

[engine]

KeyMeaning
binarypath (or bare PATH-resolved name) of the engine binary; default shape saphan

[workspace]

KeyMeaning
defaultworkspace used when a command does not name one
canonwhich tree is canon for this workspace: local or origin
worktree_grant_classthe declared permission-grant settings file every copy surface reads as its source (worktree creation and dispatch command). Relative paths resolve against the workspace root. When set, it wins over the deprecated run.grant, and the dispatch says so. ⚠ Setting this key does not mean every backend receives a grant — only claude-code does. See Which backends receive a grant.

[workspace.grant_class]

KeyMeaning
<role> (master, executor, review, design, scribe, advisor)a permission-grant class for one fleet role; overrides workspace.worktree_grant_class for spawns carrying that --role. A role with no entry falls back to it, and the fallback is recorded on the run.

[run]

KeyMeaning
binarythe agent backend binary saphan run launches (claude, codex) — distinct from engine.binary
envcomma-separated list of environment variable names to pass through to runs (e.g. HOME,PATH,USER)
lawsabsolute path of the standing-law index a composed dispatch injects. Absent = the stream loses dispatch eligibility with a named reason — never a guessed default.
grantdeprecated alias of workspace.worktree_grant_class; read only when the newer key is unset
confinementengine (the default — the engine composes the OS sandbox itself), container (the run is placed in one of Saphan Studio's container images; local machine only) or vendor (the agent tool confines itself, and the run record says the engine makes no confinement claim). Any other value refuses at spawn, before anything runs. See Sandboxing.
harnesswho stands a spawned run's working environment up: legacy (the default — the engine composes the agent's command line and nests its own jail, egress door and guardrail proxy around it) or saphan-agent (the engine composes a run specification and starts saphan-agent)
git_user_namecommit authorship a spawned leg composes with, on both the local and the ssh lanes. Not a host default: a leg without it would commit as whatever the far host is configured with, so the spawn is refused rather than let the record acquire an author nobody chose.
git_user_emailthe address half of the same pair; set both or neither satisfies the check.
role_liveness_freshnesshow recent an executor's liveness must be to count as "live" for the per-(stream, role) dispatch lock; a Go duration, default 90m
stale_canonwhat a spawn does about a canon document whose bytes differ from the law record's binding text for the same citation; hand-record (the default) hands the run the record's text
transcript_streamingstream-json for a streaming JSONL transcript; empty for the single-object form

[run.codex]

KeyMeaning
capacity_retrieshow many times the engine resumes a codex session the vendor ended on a capacity refusal, waiting the fleet's back-off between attempts; default 6 (about ten minutes of waiting in all)
request_max_retrieshow many times a refused request is retried before the codex turn ends; default 8. ⚠ Declared and validated, not yet passed to codex.
stream_idle_timeout_mshow long a silent stream is tolerated, in milliseconds; default 300000. ⚠ Declared and validated, not yet passed to codex.
stream_max_retrieshow many times a broken stream is resumed within one turn; default 10. ⚠ Declared and validated, not yet passed to codex.

Unknown keys in this section are refused by name at load.

[dispatch.effort.<role>] · [dispatch.model.<role>] · [dispatch.verbosity.<role>]

KeyMeaning
<backend>the default effort, model, or verbosity (terse, normal, narrative) a dispatch uses for that role on that backend

[store]

KeyMeaning
backendrecord backend: sqlite (default — one WAL-mode file under <workspace>/.saphan/state) or postgres for central deployments
postgres_dsn_envname of the environment variable holding the DSN — never the DSN itself

[credentials]

KeyMeaning
fileabsolute path of the one credentials file for this host — a flat JSON object of NAME → value, mode 0600, validated when the configuration loads. See Secrets.

[channel]

KeyMeaning
base_urlchannel store base URL — the proxy, or loopback on the store's own host
credentialname of the credentials-file entry carrying the channel store credential
db_namechannel store database name
read_timeoutkeyed-read deadline; a Go duration, default 15s
list_timeoutwhole-corpus read deadline; a Go duration, default 60s

Unknown keys in this section are refused by name at load.

[class]

KeyMeaning
law_db_name · work_db_name · record_db_name · telemetry_db_nameCouchDB database names for law material, working material, the record and telemetry

Unknown keys in this section are refused by name at load.

[evidence]

KeyMeaning
base_urlCouchDB evidence-store base URL (e.g. http://localhost:5984). The evidence store is an optional blob lane additive to [store]; absent configuration degrades gracefully.
db_namedatabase name
credentialname of the credentials-file entry carrying the CouchDB credentials
reader_credentialname of the entry carrying a read-only CouchDB identity; a pre-flight proves it can read and cannot write
archivetrue or false (default) — whether a stream's teardown also carries its working material out of the worktree as one signed archive
max_member_bytesthe largest single file a teardown records as itself — bytes, or with a KiB/MiB/GiB suffix; default 8MiB. A larger file is replaced by a note naming it.
max_archive_bytesthe most a teardown's evidence container carries in total; default 64MiB

[console]

KeyMeaning
legacytrue or false (default). With it absent or false, an old console screen address /console/<id> redirects to the current console at /console/app/<id>; true serves the old pages again. See Screens and routes.
alive_horizonhow long a stream with no worktree stays listed as needing you after its record last moved; a Go duration, default 168h. It hides; it never closes.
tool_permission_prompttrue or false (default) — whether the console's claude-code conversations ask the vendor to route its tool-permission prompts back to the console

[act_authority]

How a gate act is authorised, and how saphan-gateway reaches the server that records it. Unknown keys in this section are refused by name at load.

KeyMeaning
modewebauthn — the only accepted value, and what an absent section means: every act is a verified browser passkey assertion, on every tier
webauthn_rp_idthe relying-party id, a bare domain (console.example.com); no default, never inferred from the request
webauthn_originscomma list of the exact origins the console is served from, compared byte for byte; no suffix match, no wildcard
webauthn_user_verificationthe floor for ceremonies other than acts: required, preferred or discouraged — an act's challenge always requires verification
channel_urlthe gateway's dial: the https base URL of the server's private act surface
channel_client_cert · channel_client_keythe gateway's own service certificate and its key
channel_server_ca_filethe CA the server's certificate must chain to — never omitted
channel_listenthe server's listen address for that private surface
channel_server_cert · channel_server_keythe certificate that listener presents, and its key
channel_client_ca_filethe CA every admitted gateway's client certificate must chain to — required when channel_listen is set
channel_socketthe co-located alternative: an absolute Unix socket path authenticated by the kernel's peer credentials. Declaring it alongside the certificate keys is refused.
channel_peer_uidscomma list of numeric uids admitted on that socket; an empty list admits nobody

[client]

Where this CLI gets the record. Unknown keys in this section are refused by name at load.

KeyMeaning
modelocal (what an absent section means — the workspace store on this host) or gateway (HTTPS to saphan-gateway; no local store is opened)
gateway_originthe gateway's public API origin, exactly https://host[:port]; no default and never inferred
trust_rootsthe PEM bundle the gateway's server certificate must chain to — required in gateway mode
tenantthe tenant label this CLI belongs to; optional on a single-tenant deployment
client_cert · client_keythis machine's optional client certificate and key, when the deployment admits CLI machines by certificate; all or neither. It identifies the machine, never a seat or an act.
exec_originthe origin of the gateway's mutually-authenticated listener, where saphan gateway exec reaches the exec door

[oauth]

⚠ Not [oauth_server]. This section says which authorization server the engine trusts as a resource server. [oauth_server] says what the product's own authorization server is. oauth.issuer is somebody else's name; oauth_server.issuer is yours. An operator who sets one where the other belongs gets a server that trusts itself.

KeyMeaning
modeself-issued (default) or external
issuerexternal authorization-server issuer URL; required when mode = external, ignored otherwise. See Trusting an issuer.

[oauth_server]

⚠ Not [oauth]. This section configures the authorization server this product runs — the saphan-oauth binary: its own name, its own certificate, its own database.

Unknown keys in this section are refused by name at load. The first-run procedure is Starting the authorization server.

KeyMeaning
issuerRequired. This server's own address, exactly as clients use it, e.g. https://auth.example.com. It appears in every token this server signs and in both discovery documents. It is never derived from a request. Must be https://, with no query, no #fragment and no trailing slash. An http:// address is accepted only on 127.0.0.1, [::1] or localhost, for local testing.
listenhost:port to bind. Default 127.0.0.1:7655. A host-less value (":7655") is refused as ambiguous at every tier. Precedence: --listen → SAPHAN_OAUTH_LISTEN → this key → the default.
tls_modeRequired, no default. native — this server presents its own certificate. proxy — a load balancer, reverse proxy or tunnel in front of it handles encryption, and this listener speaks plain HTTP to the local machine only.
tls_cert · tls_keyPEM certificate and private key. Required when tls_mode = "native".
tls_min_version1.3 (default) or 1.2. Use 1.2 only for clients that cannot speak 1.3.
trusted_proxiesComma-separated CIDR ranges of proxies whose X-Forwarded-* and RFC 7239 Forwarded: headers may be believed. This list ADDS to loopback; it never replaces it. A range covering 0.0.0.0/0 or ::/0 is refused, and so is trusted_proxies = "" — to trust nothing beyond this machine, delete the line.
state_dirRequired. Directory this server may write: its database, its signing-key files (mode 0600), its pidfile and log.
store_backendsqlite (default, one file under state_dir) or postgres.
postgres_dsn_envname of the environment variable holding the PostgreSQL connection string. Required when store_backend = "postgres".
signup_enabledtrue opens the self-registration page /signup; absent or false keeps it closed
turnstile_site_key · turnstile_secret_envthe public Cloudflare Turnstile site key shown on /signup, and the name of the environment variable holding its secret; both required when signup_enabled
mail_transporthow sign-up mail leaves the server: cloudflare-email; unset, sign-up answers closed
mail_account_id · mail_from · mail_token_envthe Cloudflare account id, the sender address, and the name of the environment variable holding the Email API token; all required with mail_transport

Signing keys are never created automatically. A server with no published key refuses to start and names the command that fixes it.

[gateway]

This section configures saphan-gateway, which serves the console and the client contract. Unknown keys in this section are refused by name at load. See Central servers and the client-contract gateway.

KeyMeaning
listenhost:port to bind. Default 127.0.0.1:7656. Precedence: --listen → SAPHAN_GATEWAY_LISTEN → this key → the default.
namethe name this gateway answers to — the host a client dials and the name its server certificate carries, never an address it binds. Precedence: --name → this key → saphan-gateway.
enable_httpstrue to serve the console over ordinary server-authenticated TLS with tls_cert and tls_key — never mutual TLS
tls_cert · tls_keythis gateway's own server certificate and private key
mtls_listenhost:port of a second, mutually-authenticated listener carrying the bridge and evidence routes and nothing else. Needs the whole trio of client_ca_file, tls_cert and tls_key.
client_ca_filethe fleet CA every machine's client certificate must chain to — required once mtls_listen is set
exec_doortrue lets the mutually-authenticated listener run a saphan command as a child of the gateway and stream its output back. Needs mtls_listen. Off by default.
couchdb_urlthe address CouchDB listens on from this host — loopback only; a non-loopback host is refused
couchdb_proxy_secret_envnames the CouchDB proxy-auth shared secret; declaring it selects proxy-auth mode. ⚠ It is resolved as an entry in the credentials file — see Secrets.
couchdb_proxy_secret_file⚠ accepted by config set, refused as retired when the gateway reads it
couchdb_rolescomma-separated CouchDB roles a machine is granted in proxy-auth mode; empty means no roles beyond the implicit user

The --workspace the gateway serves is deliberately not a configuration key: every start names it.

[server]

⚠ Not [oauth_server]. This section configures saphan server — the engine, which is a resource server: it accepts tokens. [oauth_server] configures the authorization server that issues them.

Unknown keys in this section are refused by name at load.

KeyMeaning
listenhost:port for saphan server; a host-less value (":7654") is refused as ambiguous at every tier
public_base_urlThe origin this engine asserts as its OAuth issuer: the iss of every token it mints, the issuer and both endpoint URLs of the RFC 8414 document, the resource and authorization_servers of the RFC 9728 document, and the origin inside every WWW-Authenticate challenge. It is an origin — scheme and host, nothing else: no path, no trailing slash, no query, no #fragment, no user:password@, and http/https only; each is refused at load, by name. It is never derived from a request.
trusted_proxiesComma-separated CIDR ranges (a bare IP is read as its single-address block) of proxies whose X-Forwarded-* and RFC 7239 Forwarded: headers this engine may believe. This list ADDS to loopback. A trusted hop may report the scheme it terminated; it may not move this engine's name.

What public_base_url decides, and what happens when it is absent

Set it, and nothing a caller sends can change the name this engine asserts — not the Host: header, not X-Forwarded-Host, not RFC 7239 Forwarded: host=, not an absolute-form request target, not HTTP/2 :authority, not the TLS server name, and not a trusted proxy either.

Leave it absent, and the engine names itself from the address it observed on its own socket — the local address of the connection, which no client can set. On the solo tier (bound to 127.0.0.1:7654) that is http://127.0.0.1:7654. If a trusted hop reports X-Forwarded-Proto: https, the scheme — and only the scheme — follows it.

⚠ An engine behind a proxy MUST set this key. Unconfigured, it names itself by its own loopback address, which its clients cannot reach.

The issuer row that goes with it. Enrolling an authorization server is your act — the engine never writes that record. The row's audience is this engine's resource identifier, public_base_url + /mcp. See Trusting an issuer.

[saphan_server]

KeyMeaning
projectionon (default) or off — whether this host runs the projection service: the previous console and the MCP surface
supervisoron (default) or off — whether this host runs the fleet supervisor: the mailbox, the chat integrations, the dispatch loop

[trust]

KeyValuesNote
enforceoff | warn | enforcesigned-instruction enforcement; validated at load and at config set. Default off.
rotation_gracea Go durationhow long a superseded machine certificate stays admissible while its replacement travels to the machine; default 24h. Zero, negative and unparseable values are refused.

[org]

KeyValuesNote
supersede_policyflag | refuse-merge | owner-decidesvalidated at load and at config set. Absent means owner-decides. ⚠ This release parses and stores the policy; the merge path does not act on it yet. See Supersede policy.

[egress]

KeyMeaning
allow_unsafe_networkstrue or false — whether an egress policy document may name the private ranges (RFC 1918, IPv6 ULA). It opens nothing on its own; loopback and the cloud metadata endpoint stay refused either way. See Egress control.

[guardrail]

The model-text guardrail. Its full description, and what each mode can stop, is on Guardrails.

KeyMeaning
recordrecord every session: true or false — absent means true, the one key in this section that is on by default. See Session recording.
enabledarm the guardrail: true or false (default). true is refused unless the plugin keys below are set.
backendscomma list of the backends it covers; empty covers nothing
seatscomma list of seat ids to narrow it to; empty means every seat of a covered backend
request_mode · response_modeobserve (default — records every decision, stops nothing) or enforce. ⚠ enforce on the response buffers every response before the client sees it.
handoff_modeobserve or enforce for the bytes a leg is handed at spawn. ⚠ In enforce it can refuse every spawn on the fleet.
ingress_modeobserve or enforce for what enters the bridge
enforce_actsabsolute path of the file of this host's signed owner acts that enforcing modes require
plugin_manifest · plugin_artifact · plugin_digestthe plugin's manifest and executable on the host the proxy runs on, and your pin over the manifest's exact bytes (64 lower-case hex digits)
listenthe proxy's fixed bind address, 127.0.0.1:<port>

[guardrail.upstream] · [guardrail.codec]

KeyMeaning
<backend>for one backend: the provider base URL the proxy stands in front of (the same one that backend's client is configured with), and the wire codec the proxy speaks

[headroom]

KeyMeaning
warn_usdwarning threshold for the fleet-capacity pre-flight; empty means the built-in default 5.00 USD. A warning flags; it never blocks.

[probe.model]

KeyMeaning
claude-codecomma-separated models tried, in order, for each Claude Code seat; default opus
codexcomma-separated models tried, in order, for each Codex seat; default gpt-5.6-sol
saphan config set probe.model.claude-code "opus,fable"
saphan config set probe.model.codex "gpt-5.6-sol,gpt-5.5"

The list distinguishes "this seat is spent" from "this seat is spent for this model." fleet headroom stops at the first model that answers and reports that model plus every earlier model that did not. Each failed model before the answer is another real, potentially billable probe. An empty list is refused, and any backend other than claude-code or codex is refused by name at both load and config set. fleet headroom --model <name> remains a one-model override for every backend for that invocation.

[scheduler]

KeyMeaning
host_max_liveper-host ceiling on concurrently live runs; default 6
account_max_liveper-vendor-account ceiling on live runs. It is declared, not measured: no vendor is asked for its concurrency limit.
exhausted_window_hourshow long a lane-exhausted observation keeps a lane out of consideration; default 5.0
usage_probe_after_exhausted⚠ parsed and shown, acted on by nothing yet

[scheduler.<backend>]

KeyMeaning
account_max_livethe same ceiling for one backend, above the general one

Unknown keys in a [scheduler.<backend>] section are refused by name at load.

[fleet]

KeyValuesNote
ssh_host_key_policyrefuse | accept_rotationvalidated at load and at config set; defaults to refuse. See SSH host key policy.
usage_refresh_minutesminuteshow often the server refreshes the fleet's free usage readings; default 15, 0 disables

[fleet.verify.class] · [fleet.verify.family]

The model families and classes a verification may count as independent of the work it verifies. Unknown sections and keys here are refused by name at load; read saphan config set --help for the shape your build takes.

[ssh]

KeyMeaning
connect_timeouta far host's ssh ConnectTimeout; a Go duration 1s–1h, default 15s
probe_connect_timeoutthe connect bound of the admission-time key probe; default 5s
identity_probe_timeoutthe far identity and pre-flight probe's whole round trip; default 30s
round_trip_timeoutone far round trip — a return fetch, an artifact stat, a provision write; default 2m. A slow, relayed link needs more.
server_alive_interval · server_alive_count_maxkeepalives for far sessions; defaults 15s and 8
multiplexon or off (default) — share one ssh connection between short far calls; a leg's long session is never multiplexed
control_persisthow long a multiplexed master connection stays after its last call; default 60s

[host.<id>] — repeated section, keyed by machine id

KeyValuesNote
ssh_host_key_policyas aboveoverrides [fleet] for one machine; empty means inherit
ssh_connect_timeout, ssh_probe_connect_timeout, ssh_identity_probe_timeout, ssh_round_trip_timeout, ssh_server_alive_interval, ssh_server_alive_count_max, ssh_multiplex, ssh_control_persistas in [ssh]the same settings for one machine — e.g. saphan config set host.runner-1.ssh_round_trip_timeout 3m

[lane.<alias>] — repeated section, keyed by alias

KeyMeaning
base_urllane endpoint (an OpenAI-compatible root, e.g. a LiteLLM proxy, Ollama, vLLM)
api_protocolRequired: openai-chat, openai-responses or anthropic-messages. Never inferred from base_url; a lane without it refuses at load.
credentialname of the credentials-file entry carrying the lane's key
providergeneric-openai or openrouter — which dialect the options below follow
openrouter_order · openrouter_only · openrouter_ignorecomma lists of provider ids: the order to try them, the only ones allowed, or ones never to use (only and ignore are mutually exclusive)
openrouter_allow_fallbacks · openrouter_require_parameterstrue or false
openrouter_data_collectiondeny or allow — whether the routed provider may retain prompts
openrouter_http_referer · openrouter_app_titleattribution sent on every call

A [lane] with no alias, or a [host] with no id, is a named load failure, not an ignored block. Unknown keys in a lane section are refused by name at load.

[forge] · [forge.<id>] · [repo.<inventory-name>]

The forge connection behind Pull requests: its kind (gitea or github), API base URL, repository, the lane aliases whose credentials are its writer and approver tokens, its enforcement and merge method — either as one [forge] section or as named connections bound per repository. The keys are listed by saphan config set --help, and saphan config forge shows and migrates them.

[verify.full] · [verify.affected] · [verify.provision.<alias>]

KeyMeaning
argv · report · report_path · timeoutthe full (or affected-path) verification command, its report kind and file, and its wall-clock limit
source · desta document the suite relies on, and where it lands in the measured tree

[round]

KeyMeaning
after_mergehow a round opened after its stream is merged reaches a gate: new-stream (default) or reopen

[otel]

KeyMeaning
enabledexport the record's metrics to an OTLP/HTTP collector: true or false (default)
endpoint_urlthe collector's metrics endpoint, http or https, with no user:password@ and no query
endpoint_nameyour label for the collector — the only endpoint identity that reaches the metrics

See Metrics export.

[license]

KeyMeaning
portal_urlthe portal's base URL; absent means the production portal
dev_lease_key_filedevelopment and testing only; refused by a release build

The [notice…] sections — the outbound chat notices

These are the keys behind Notifications: a binding is a destination plus the credential and rendering to reach it, a route says which notice type goes to which binding, and one key names the single machine that does the sending. <name> is a binding name you choose; <n> is a route number — both belong to the section header.

[notice.binding.<name>]

KeyMeaning
sinkthe kind of destination: slack
destinationthe conversation the sink posts to — a channel or DM id. ⛔ A user id is refused at set time; the Slack page says why
formathow the notice is rendered: plaintext
credentialname of the credentials-file entry carrying the bot token
localewhich released catalogue renders it; a draft catalogue is refused
timezonean IANA zone name, shown beside UTC wherever a notice carries a time
deliver_fromnow (default — a new binding starts at the moment it is first considered) or all (it also carries the undelivered backlog)
no_reply_linetrue turns on the product's own line saying this conversation does not take replies
noteone line of your own, at most 200 characters, no line break, no braces, no control characters — refused, never truncated

[notice.route.<n>]

KeyMeaning
typethe registered notice type this route carries; an unregistered type is refused. See What goes out.
severity_mininfo, notable, red or urgent — the lowest severity carried; omit to carry all
bindingwhich binding it delivers through; two routes may send the same type to different bindings
actornarrows the route to one actor; omit to match every actor

[notice.sender]

KeyMeaning
primarythe operating system hostname of the one machine that sends — what hostname prints on that host; a second instance refuses by name. ⛔ It is not the registry machine id that keys [host.<id>]; the two are the same only when nobody overrode the id and hostname prints the short form. The Slack page works both cases.

[notice.catalogue.<locale>]

KeyMeaning
pathabsolute path of a JSON notice catalogue your workspace brings — for a language the binary was not built with, or your own phrasing of one it was