Skip to main content

Secrets management

WednesdayAI supports SecretRefs so supported credentials do not have to live as plaintext in your config. SecretRefs are opt-in, additive, and per-credential — plaintext still works everywhere it did before. A SecretRef points the gateway at a credential held in an environment variable, a local file, or an external resolver command (1Password, Vault, sops, and so on). The credential is resolved into an in-memory snapshot at activation time, never on the hot request path.

Runtime model

  • Resolution is eager during activation, not lazy on each request.
  • Startup fails fast if an effectively active SecretRef cannot be resolved.
  • Reload uses an atomic swap: full success, or the last-known-good snapshot is kept.
  • Requests read only from the active in-memory snapshot.
This keeps secret-provider outages off the request path. If a reload fails after a healthy start, the gateway enters a degraded state (event SECRETS_RELOADER_DEGRADED) and keeps serving from the last good snapshot until a later reload recovers (SECRETS_RELOADER_RECOVERED).

Active-surface filtering

SecretRefs are only validated on surfaces that are actually in use. A ref on a disabled channel, an unselected web-search provider, or an inactive auth path does not block startup — it emits a non-fatal diagnostic (SECRETS_REF_IGNORED_INACTIVE_SURFACE) instead. Gateway auth surfaces (gateway.auth.password, gateway.remote.token, gateway.remote.password) log their state explicitly as active or inactive under SECRETS_GATEWAY_AUTH_SURFACE, with the reason, so you can see why a credential was required or skipped.

SecretRef contract

Every SecretRef uses the same object shape:
{ source: "env" | "file" | "exec", provider: "default", id: "..." }
{ source: "env", provider: "default", id: "OPENAI_API_KEY" }
  • provider matches ^[a-z][a-z0-9_-]{0,63}$
  • id matches ^[A-Z][A-Z0-9_]{0,127}$
  • Missing or empty env values fail resolution.

Defining providers

Providers live under secrets.providers, with defaults selecting one provider per source:
{
  secrets: {
    providers: {
      default: { source: "env" },
      filemain: {
        source: "file",
        path: "~/.openclaw/secrets.json",
        mode: "json", // or "singleValue"
      },
      vault: {
        source: "exec",
        command: "/usr/local/bin/openclaw-vault-resolver",
        args: ["--profile", "prod"],
        passEnv: ["PATH", "VAULT_ADDR"],
        jsonOnly: true,
      },
    },
    defaults: { env: "default", file: "filemain", exec: "vault" },
  },
}
The file provider reads path: mode: "json" resolves id as a JSON pointer; mode: "singleValue" returns the whole file for id: "value". The exec provider runs an absolute binary path; by default the command must be a regular file (not a symlink). For package-manager shims (such as Homebrew), set allowSymlinkCommand: true and pair it with trustedDirs.
On Windows, secret resolution fails closed when ACL verification is unavailable for a file or command path. For trusted paths only, set allowInsecurePath: true on that provider to bypass path security checks.

Exec integration examples

{
  secrets: {
    providers: {
      onepassword_openai: {
        source: "exec",
        command: "/opt/homebrew/bin/op",
        allowSymlinkCommand: true,
        trustedDirs: ["/opt/homebrew"],
        args: ["read", "op://Personal/OpenClaw API Key/password"],
        passEnv: ["HOME"],
        jsonOnly: false,
      },
    },
  },
  models: {
    providers: {
      openai: {
        baseUrl: "https://api.openai.com/v1",
        models: [{ id: "gpt-5", name: "gpt-5" }],
        apiKey: { source: "exec", provider: "onepassword_openai", id: "value" },
      },
    },
  },
}
{
  secrets: {
    providers: {
      vault_openai: {
        source: "exec",
        command: "/opt/homebrew/bin/vault",
        allowSymlinkCommand: true,
        trustedDirs: ["/opt/homebrew"],
        args: ["kv", "get", "-field=OPENAI_API_KEY", "secret/openclaw"],
        passEnv: ["VAULT_ADDR", "VAULT_TOKEN"],
        jsonOnly: false,
      },
    },
  },
}
{
  secrets: {
    providers: {
      sops_openai: {
        source: "exec",
        command: "/opt/homebrew/bin/sops",
        allowSymlinkCommand: true,
        trustedDirs: ["/opt/homebrew"],
        args: ["-d", "--extract", "[\"providers\"][\"openai\"][\"apiKey\"]", "/path/to/secrets.enc.json"],
        passEnv: ["SOPS_AGE_KEY_FILE"],
        jsonOnly: false,
      },
    },
  },
}

Precedence

  • A field without a ref is left unchanged.
  • A field with a ref is required on active surfaces during activation.
  • If both plaintext and a ref are present, the ref wins on supported precedence paths and the gateway logs SECRETS_REF_OVERRIDES_PLAINTEXT.
  • When auth-profiles.json credentials shadow an openclaw.json ref, the audit reports REF_SHADOWED.
Runtime-minted or rotating credentials and OAuth refresh material are intentionally excluded from read-only SecretRef resolution. Use the auth profile flows for those.

Audit and configure workflow

The standard operator flow:
openclaw secrets audit --check
openclaw secrets configure
openclaw secrets audit --check
  • secrets audit reports plaintext values at rest (in openclaw.json, auth-profiles.json, .env), unresolved refs, precedence shadowing, and legacy residues.
  • secrets configure is an interactive helper: set up secrets.providers, pick the secret-bearing fields to convert, capture the SecretRef details, run preflight resolution, and optionally apply. Useful modes: --providers-only, --skip-provider-setup, --agent <id>.
Applying a saved plan:
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run
After rotating a backend secret, refresh the gateway snapshot:
openclaw secrets reload

One-way safety policy

WednesdayAI deliberately does not write rollback backups containing historical plaintext secrets. Preflight must succeed before write mode, runtime activation is validated before commit, and applies use atomic file replacement with best-effort restore on failure. When you convert a field to a SecretRef, configure --apply scrubs the matching plaintext from auth-profiles.json, legacy auth.json, and <config-dir>/.env.
Related: Authentication · Gateway configuration · Security hardening