Saphan StudioDocs
Configuration

Secrets

The file holds names and paths, never values.

The configuration file holds NAMES, never values. A configuration file that contains a secret is a payload, not configuration.

The one credentials file

Every credential the engine itself reads lives in one file per host, credentials.json: a flat JSON object of NAME → value, mode 0600. The configuration names that file once, and each section that needs a credential names an entry in it:

[credentials]
file = "/home/alice/.saphan/etc/credentials.json"

[lane.local]
credential = "LITELLM_API_KEY"
{ "LITELLM_API_KEY": "…the key itself…" }
keywhat it carries
credentials.filethe absolute path of the file. A relative path is refused
channel.credential · evidence.credential · evidence.reader_credential · lane.<alias>.credential · notice.binding.<name>.credentialthe name of an entry in that file — shaped [A-Za-z_][A-Za-z0-9_]*. Anything else (a pasted user:password or sk-…) is refused as credentials-name-not-a-name, and the refusal does not print what you typed

The file is validated when the configuration loads, not when a credential is first needed: every verb except config set refuses by name until the file and the configuration agree. The value is then read fresh at the moment of use, so a file re-chmodded or rewritten later is caught too. The credential value never appears in any message, argv, or record — refusals name the key, the entry name and the file path, nothing else.

refusalwhat it means
credentials-file-undeclareda section names a credential and no credentials.file is set
credentials-file-not-absolutecredentials.file is relative, or padded with whitespace
credentials-file-absentthe declared file is not on this host
credentials-file-not-regularthe path is a directory, FIFO, socket or device — refused before it is opened, because opening one can block
credentials-file-modethe file is not mode 0600 — fix with chmod 600 <file>
credentials-file-unreadablethe file is regular and 0600 and still could not be read
credentials-file-not-json · credentials-file-not-an-objectthe content is not a flat JSON object (the parser's message is withheld, because it would quote the bytes)
credentials-value-not-a-string · credentials-value-empty · credentials-name-emptyan entry is malformed
credentials-name-unknowna section names an entry the file does not carry; the refusal lists the names it does carry
credentials-key-retiredthe configuration uses an older per-section spelling (credentials_env, credentials_file, couchdb_credentials_env …); the refusal names the key that replaces it and what to do with the old file

For a lane, two more: lane-unknown (no such lane is declared — the refusal lists the declared ones) and lane-credentials-undeclared (the lane declares no credential).

A lane's credential does not travel. When a dispatch carries a lane declaration to another host, only the base_url, the credential's NAME and the api_protocol travel; the execution host resolves the value from its own credentials file, and refuses lane-credential-not-on-this-host before the first model call if it cannot.

Keys that still name an environment variable

A few server keys name an environment variable rather than an entry: store.postgres_dsn_env, and in [oauth_server] postgres_dsn_env, turnstile_secret_env and mail_token_env. The variable must be set in the environment of the process that reads it.

⚠ gateway.couchdb_proxy_secret_env is resolved as an entry in the credentials file, not as an environment variable, although its help text says otherwise; and gateway.couchdb_proxy_secret_file is accepted by config set but refused as retired when the gateway uses it. Put the proxy secret in credentials.json and name its entry in gateway.couchdb_proxy_secret_env.

On this page