Skip to content

Secrets references

A Matrix package's configuration carries tunables — log levels, timeouts, feature flags, limits. It does not carry secrets. Credentials, API tokens, OAuth refresh tokens, and NATS user/passwords go through system.factotum. This page is the boundary spec.

The rule, in one paragraph

Inference credentials NEVER leave the local machine. system.factotum is the ONE actor that reads/writes credential files. Other actors NEVER touch credentials directly — they request operations through Factotum.

This is rule #4 of the project-root CLAUDE.md. It applies to:

  • LLM provider keys (Anthropic, OpenAI, Google, Codex).
  • OAuth refresh tokens.
  • HiveCast pairing tokens / Device-Link credentials.
  • Any third-party API key your actor uses.
  • Database passwords.
  • Hetzner / cloud-provider tokens used by deployment helpers.

Things that are NOT secrets and CAN go in regular config:

  • Public API endpoints (https://api.openai.com/v1).
  • Model IDs (claude-opus-4-7).
  • Default behaviour flags.
  • Numeric limits.

Why credentials never appear in config.defaults

The defaults file ships in the package on disk. Anyone reading the source tree, opening the npm tarball, or browsing the install directory sees it. The OAuth/Codex token-rotation thesis (memory.md, 2026-04-02) was that user-supplied credentials, including OAuth tokens for Anthropic and OpenAI, must never leave the machine they were created on. Putting them in config.defaults.json violates that immediately — and they would also be wrong: defaults are per-package, credentials are per-installation/per-user.

Why credentials never appear in env vars (well, mostly)

Env vars are the most-tempting place to put a secret: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc. They are accessible to every process the user runs, leak through ps, and end up in shell history if someone does OPENAI_API_KEY=sk-... node script.js. The Matrix model is to avoid this entirely:

  • Inference providers run inside actors that ask Factotum for the credential by name.
  • Factotum reads the credential file (~/.matrix/credentials/, scoped per provider).
  • The credential never reaches the package's own runtime memory unless the actor explicitly fetches it for a single call.

The exception: bootstrap-only secrets like MATRIX_NATS_TOKEN for the local NATS connection during installer flows can live in env vars because they are one-shot and scoped to a specific install step. Long-lived secrets do not.

How a package consumes a credential

Through system.factotum. The package config refers to the credential by name, not by value:

json
{
  "auth": {
    "provider": "anthropic",
    "credentialRef": "anthropic-oauth"
  }
}

The actor looks up the value at runtime:

ts
const cred = await RequestReply.request<{ token: string }>(
  this._context!.transport,
  'system.factotum',
  'factotum.read',
  { name: 'anthropic-oauth' },
);

await callAnthropicApi({ token: cred.token, ... });

factotum.read returns the credential value scoped to the requester's principal. The credential never appears in any config file, any env var, any log line, any $introspect response, any persisted state.

Source: WORKSTREAMS/product-launch/HIVECAST-DEVICE-ENROLLMENT-SPEC.md for the OAuth/Device-Link credential lifecycle. The Factotum actor's contract is documented in @open-matrix/system-factotum.

What goes in credentialRef

ReferenceMeaning
"anthropic-oauth"The Factotum credential named anthropic-oauth.
"openai-key"The Factotum credential named openai-key.
"github-token"The Factotum credential named github-token.

By convention, names are lowercase, hyphen-separated, and namespaced with the provider. The actual storage is in <host-home>/credentials/, written and read only by Factotum.

Pattern: per-user vs per-package credentials

Some credentials are user-supplied (a user's OpenAI API key). Others are package-supplied for shared backend services. Both go through Factotum, but with different scopes:

  • Per-user credentials. Stored under the user's principal. Only available to actors running on behalf of that user.
  • Per-installation credentials. Stored under the install. Available to system actors (e.g., the local NATS user/pass that gateway and runtimes share).

Packages should not care which scope a credential came from — they ask Factotum, Factotum returns the value scoped to whoever's making the request.

Things you must not do

  • Do not log a credential, even at debug level. Log lines end up in logs end up in support tickets.
  • Do not put a credential in actor state. State is observable via $getState.
  • Do not return a credential from an op handler. It surfaces in callers' logs.
  • Do not put a credential in a setState call. Same reason.
  • Do not pass a credential through a $prompt payload. It will end up in the agent's transcript.

If you find a credential leaking, treat it as a security incident and rotate the credential immediately.

See also

Source: Root CLAUDE.md § "THE RULES" #4 (Credentials go through Factotum ONLY), WORKSTREAMS/product-launch/HIVECAST-DEVICE-ENROLLMENT-SPEC.md (link credentials), @open-matrix/system-factotum actor contract.