Skip to content

Secrets & the credential guard

How to put secrets in a profile's config.env, how redaction scrubs them from logs and events, and why a profile that sets an auth credential is rejected.

This page builds on profiles. Read that first if you have not. For the full safety model, see security.

Two kinds of env value

A profile's config.env maps a name to one of two value shapes:

Shape Example Meaning
Literal "NODE_ENV": "production" A plain string, stored as-is in the config file.
SecretRef "OPENAI_TOKEN": { "secretRef": "OPENAI_TOKEN" } A reference. The real value is resolved at session-create time, never written to the config.

A SecretRef is just { "secretRef": "<NAME>" }. The config holds the name, not the secret. The value is supplied at runtime by a SecretsProvider.

{
  "name": "agent",
  "transport": "sdk",
  "config": {
    "env": {
      "NODE_ENV": "production",
      "OPENAI_TOKEN": { "secretRef": "OPENAI_TOKEN" }
    }
  }
}

Tip

Prefer a SecretRef for anything sensitive. It keeps the literal out of the config file, and it is redacted from events at any length (see residual).

The SecretsProvider

A SecretsProvider resolves a ref name to a plaintext value (or undefined). It is a synchronous seam:

interface SecretsProvider {
  resolve(ref: string): string | undefined;
}

Two built-ins ship in the public API (claude-coder):

Provider Behavior When
NULL_SECRETS Every ref resolves to undefined. Default. Safe and inert: a SecretRef whose key cannot resolve is simply dropped from the env.
PROCESS_ENV_SECRETS resolve(ref) returns process.env[ref]. Opt-in. Pull secrets from the host process environment.

The provider is wired in via the library SessionManager. The CLI uses the default (NULL_SECRETS).

import { SessionManager, PROCESS_ENV_SECRETS } from '@ahmed-hobeishy/claude-coder';

const manager = new SessionManager({
  secrets: PROCESS_ENV_SECRETS, // opt into process.env resolution
});

A custom provider (Vault, a keychain, etc.) is any object with resolve:

const vault: SecretsProvider = {
  resolve: (ref) => myVault.get(ref), // string, or undefined if absent
};
const manager = new SessionManager({ secrets: vault });

Note

Resolution runs at session-create time, and its output is re-checked by the credential guard (below). A provider that returns a value for, say, ANTHROPIC_API_KEY is still rejected.

What redaction does

Resolved secret values are scrubbed from published events, diagnostics, and error messages. Redaction covers two things:

  1. Resolved secret values — the plaintext a SecretRef resolved to, and literal values that look like secrets.
  2. Embedded credentials in connection strings — a postgres://user:hunter2pw@host value also has the hunter2pw fragment scrubbed on its own, so a downstream error that echoes only the password is still covered. The fragment is added separately only when it is long enough to pass the same length gate as a literal (see residual); a 2-char password like pw would not be.

What triggers redaction of a literal value:

  • A credential-shaped key name (TOKEN, KEY, SECRET, PASSWORD, PWD, AUTH, DSN, WEBHOOK, PIN, and similar).
  • A DSN or connection string with embedded credentials.
  • A known token prefix (sk-, ghp_, AKIA, eyJ, and similar).
  • A high-entropy token (long, mixed case and digits).

A resolved SecretRef is always redacted — no signal needed.

Residual: short, low-entropy literals

Redaction scrubs by blind substring replacement across every event string. A very short or common literal (ab, 1234, secret) cannot be scrubbed that way without corrupting unrelated text — code, paths, line numbers, model output — wherever that substring appears. So a literal is only added to the global scrub set if it is specific enough: length ≥ 8, or length ≥ 6 with a non-lowercase character.

That leaves a documented residual: a short, low-entropy literal secret is not auto-scrubbed.

Warning

The fix is a SecretRef. The explicit opt-in bypasses the length gate and is redacted at any length. If you must keep a short secret, reference it instead of inlining it.

The credential guard

Auth must always be the subscription OAuth that claude obtains via /login. It is injected by the runtime, never by a profile. So a profile that sets any of these env vars is rejected:

Env var Why it is rejected
ANTHROPIC_API_KEY Explicit API key — bypasses subscription OAuth.
ANTHROPIC_AUTH_TOKEN Bearer token override.
ANTHROPIC_BASE_URL Endpoint redirect.
AWS_BEARER_TOKEN_BEDROCK Bedrock bearer token.
CLAUDE_CODE_USE_BEDROCK / CLAUDE_CODE_USE_VERTEX / CLAUDE_CODE_USE_FOUNDRY Cloud-provider toggles that redirect auth.

The guard runs both on literal entries (when the profile is compiled) and on resolved entries (after a SecretsProvider runs), so a provider-injected credential is caught too. The error is credential_in_profile:

claude-coder list --config ./bad.config.json
profile 'bad' sets the credential/auth-redirect env var 'ANTHROPIC_API_KEY', which would override the subscription OAuth source; remove it (the subscription token is injected by the runtime, never by a profile)

The command exits non-zero (1). This is fail-closed: the subscription source is a hard constraint, not a default you can override.

Note

These are credentials, not secrets. They are rejected outright — not redacted. Use the subscription login (claude /login). A non-auth secret your code needs (an OpenAI key, a database password) belongs in config.env as a SecretRef, and that is allowed.

Programmatic helpers

For library use, the mechanisms on this page are exported from the claude-coder barrel: resolveEnv (substitute SecretRefs and collect the values to scrub), redactSecrets (the structure-safe global scrub), assertNoCredentialEnv + CREDENTIAL_ENV_DENY (the credential guard and its deny set), the providers NULL_SECRETS / PROCESS_ENV_SECRETS, and the EnvValue / SecretRef / SecretsProvider types.

See also

  • profiles — the config.env schema and where it lives.
  • security — the full defense-in-depth model, including the transport-level env strip.
  • library — wiring a SecretsProvider into SessionManager.