Skip to content

Managing Secrets in OpenClaw

I’ve been there—you’re about to push a config change and you realize your API keys are sitting right there in plain text. It’s a common headache. You want to keep your secrets safe without making your configuration impossible to manage or risking a leak.

OpenClaw handles this with SecretRefs. Instead of hardcoding credentials, you can point to environment variables, files, or external commands. This keeps your sensitive data in memory and away from your disk-based config files.

  • An active OpenClaw installation.
  • Credentials stored in one of the supported sources:
    1. Environment variables.
    2. JSON files.
    3. External secret managers (via executable commands).
    4. Interactive onboarding access.

I recommend using the standard SecretRef object. It follows the same shape everywhere in the system:

{ source: "env" | "file" | "exec", provider: "default", id: "..." }

This is the simplest way to get started. Set your id to the name of the environment variable.

{ source: "env", provider: "default", id: "OPENAI_API_KEY" }
  • Provider: Must match ^[a-z][a-z0-9_-]{0,63}$.
  • ID: Must match ^[A-Z][A-Z0-9_]{0,127}$.

If you store secrets in a JSON file, use a JSON pointer for the id.

{ source: "file", provider: "filemain", id: "/providers/openai/apiKey" }
  • ID: Must be an absolute JSON pointer starting with /.
  • Escaping: Use ~0 for ~ and ~1 for /.

For setups with tools like Vault, use the exec source.

{ source: "exec", provider: "vault", id: "providers/openai/apiKey" }
  • ID: Must match ^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$.

When you run onboarding in interactive mode, OpenClaw performs a preflight check. It validates your environment variable names or provider IDs before saving. If the value is empty or the provider fails, the system shows an error so you can fix it immediately.

I like that OpenClaw is designed to fail fast. Secrets are resolved into an in-memory snapshot during startup. If an active SecretRef cannot be resolved, the system won’t start.

When you reload the configuration, the system uses an atomic swap. It either succeeds completely or stays on the last-known-good snapshot. This design keeps secret provider outages from breaking your active request paths.

If things aren’t working as expected, check your logs for these specific diagnostic codes:

  • SECRETS_REF_IGNORED_INACTIVE_SURFACE: This means your SecretRef is on an inactive surface, like a disabled account or a tool that isn’t selected. The system ignores these to avoid unnecessary startup failures.
  • SECRETS_GATEWAY_AUTH_SURFACE: This log entry tells you if a gateway credential is active or inactive. For example, if you set OPENCLAW_GATEWAY_TOKEN in your environment, it wins over the config file, making the SecretRef in your config inactive.

If you are using gateway.remote.token, remember it only becomes active if gateway.mode=remote, a remote URL is configured, or you are using Tailscale serve or funnel modes.

Need more help with your specific configuration? Talk to our AI Setup Assistant.

I have spent too many hours debugging why a secret didn’t load because one service expected an environment variable while another looked for a local JSON file. It gets messy fast when you have to manually handle different logic for every secret source in your stack.

I prefer having a single, unified way to define where secrets come from. It keeps the configuration clean and ensures that your application doesn’t care if a secret lives in a .env file or a secure vault.

  • An OpenClaw configuration file.
  • Access to environment variables, local secret files, or external vault binaries.

The best way to start is by defining your providers under the secrets.providers object. I recommend setting up a default for environment variables and specific providers for files or external tools.

Here is a complete configuration example from the documentation:

{
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",
},
resolution: {
maxProviderConcurrency: 4,
maxRefsPerProvider: 512,
maxBatchBytes: 262144,
},
},
}

The environment provider is straightforward. You can use an optional allowlist to restrict which variables are accessible. Just keep in mind that missing or empty environment values will cause the resolution to fail.

This provider reads from a local file at a specific path. I use two different modes depending on the file structure:

  • mode: "json": Use this when the file is a JSON object. It resolves the id as a pointer.
  • mode: "singleValue": Use this if the file contains only the secret. It expects the ref id to be "value".

Security is strict here. The path must pass ownership and permission checks. On Windows, if ACL verification is unavailable for a path, resolution fails by default. If you are working with a trusted path and need to bypass these checks, set allowInsecurePath: true.

This runs an absolute binary path directly without a shell. By default, the command cannot be a symlink. If you need to use symlinks (like Homebrew shims), you must set allowSymlinkCommand: true and pair it with trustedDirs (for example, ["/opt/homebrew"]).

The exec provider supports:

  • Timeouts and no-output timeouts.
  • Output byte limits.
  • Environment allowlists.
  • Trusted directories.

The communication happens via stdin and stdout. Here is the request payload format:

{ "protocolVersion": 1, "provider": "vault", "ids": ["providers/openai/apiKey"] }

The expected response on stdout looks like this:

{ "protocolVersion": 1, "values": { "providers/openai/apiKey": "<openai-api-key>" } } // pragma: allowlist secret

If something goes wrong with a specific ID, the executable can return optional per-id errors:

{
"protocolVersion": 1,
"values": {},
"errors": { "providers/openai/apiKey": { "message": "not found" } }
}
  • Windows ACL Failures: If resolution fails because ACL verification is unavailable, set allowInsecurePath: true for that specific provider.
  • Symlink Command Errors: If your command points to a symlink and fails, ensure allowSymlinkCommand: true is enabled and the directory is in trustedDirs.
  • Missing Env Values: If an environment variable is empty or missing, the provider will fail the resolution process.
  • Exec Path Issues: The command must be an absolute path to a regular file unless symlinks are explicitly allowed.

If you hit a wall while setting this up, check out the AI Setup Assistant for help.

I’ve spent too much time worrying about where my API keys live. Keeping them in plain text is a bad idea, but copying and pasting them from a manager every time I run a command is just as frustrating. I prefer a setup where the tool handles the handshake with my secret manager directly.

The exec provider is my favorite way to handle this. It lets you run a local command to fetch a secret right when you need it. This keeps your keys out of your shell history and off your disk in plain text.

  • A local installation of your secret manager CLI (1Password, Vault, or sops).
  • Your configuration file ready for provider blocks.

You can set up an exec provider for different tools. Here is how I configure the most common ones.

If you use 1Password, you can point the command to your op binary. This example pulls a password from a specific vault item.

{
secrets: {
providers: {
onepassword_openai: {
source: "exec",
command: "/opt/homebrew/bin/op",
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
trustedDirs: ["/opt/homebrew"],
args: ["read", "op://Personal/OpenClaw QA 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" },
},
},
},
}

For those using Vault, you need to pass your address and token through the environment so the CLI can authenticate.

{
secrets: {
providers: {
vault_openai: {
source: "exec",
command: "/opt/homebrew/bin/vault",
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
trustedDirs: ["/opt/homebrew"],
args: ["kv", "get", "-field=OPENAI_API_KEY", "secret/openclaw"],
passEnv: ["VAULT_ADDR", "VAULT_TOKEN"],
jsonOnly: false,
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [{ id: "gpt-5", name: "gpt-5" }],
apiKey: { source: "exec", provider: "vault_openai", id: "value" },
},
},
},
}

If you prefer encrypted files in your repository, sops works great. I use the --extract flag to grab the specific key from the encrypted JSON.

{
secrets: {
providers: {
sops_openai: {
source: "exec",
command: "/opt/homebrew/bin/sops",
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
trustedDirs: ["/opt/homebrew"],
args: ["-d", "--extract", '["providers"]["openai"]["apiKey"]', "/path/to/secrets.enc.json"],
passEnv: ["SOPS_AGE_KEY_FILE"],
jsonOnly: false,
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [{ id: "gpt-5", name: "gpt-5" }],
apiKey: { source: "exec", provider: "sops_openai", id: "value" },
},
},
},
}

If you are using Homebrew on macOS, your binaries are often symlinked. If the command fails to execute, ensure you have set allowSymlinkCommand: true in your provider configuration.

Commands like vault or sops often require specific environment variables (like VAULT_ADDR or SOPS_AGE_KEY_FILE) to function. If the execution fails, check that these are included in the passEnv array.

If you hit a wall with your specific setup, check out the AI Setup Assistant.

I’ve spent too many hours tracking down why a config change didn’t pick up a new key or worrying about plaintext secrets sitting in configuration files. It is a common headache when you want to move toward more secure, reference-based credential management without breaking your existing setup. I want to show you how OpenClaw handles this through the SecretRef system so you can keep your environment clean and your credentials secure.

The core idea here is simple: if you provide a reference (a SecretRef), OpenClaw prefers it over a plaintext value. This allows you to transition away from hardcoded strings.

  1. Check your fields: If a field has a ref, it is required on active surfaces during activation.
  2. Understand Precedence: If you provide both a plaintext value and a ref, the ref takes precedence. For example, in Google Chat configurations, serviceAccountRef always wins over a plaintext serviceAccount.
  3. Trigger Activation: Secrets aren’t just loaded once. They activate during:
    • Initial Startup (preflight and final activation).
    • Config reload (both hot-apply and restart-check paths).
    • Manual triggers via the secrets.reload command.
  4. Monitor the Swap: When activation succeeds, OpenClaw swaps the secret snapshot atomically. If it fails during startup, the gateway aborts to prevent running in an insecure state.

Not every command treats secrets the same way. I find it helpful to group them into two categories based on how they react when a secret is missing.

Commands like openclaw memory (for remote-memory paths) and openclaw qr --remote are “strict.” They read from the active snapshot and will fail fast if a required SecretRef is unavailable. This prevents the system from attempting operations with incomplete credentials.

Other commands are more forgiving. If you run openclaw status, openclaw status --all, openclaw channels status, or openclaw channels resolve, the system prefers the active snapshot but won’t quit if a ref is missing. Instead:

  • They try a targeted local fallback if the gateway is unavailable.
  • If the secret is still missing, the command continues with “degraded” output.
  • You will see a diagnostic message like “configured but unavailable in this command path.”

This behavior ensures that your diagnostic tools still work even if the gateway is having trouble resolving a specific secret.

When things go wrong, OpenClaw uses specific signals to tell you what happened.

  • SECRETS_REF_OVERRIDES_PLAINTEXT: You’ll see this runtime warning if you have both a plaintext value and a ref defined. The ref is being used, and the plaintext is ignored.
  • REF_SHADOWED: This is an audit finding. It happens when credentials in auth-profiles.json take precedence over your openclaw.json refs.
  • SECRETS_RELOADER_DEGRADED: If a reload fails after the system was already healthy, it enters a degraded state. It keeps the last-known-good snapshot so your service doesn’t go down.
  • SECRETS_RELOADER_RECOVERED: You will see this once after the next successful activation following a failure.

If you rotate a secret on the backend, remember to run openclaw secrets reload to refresh the gateway snapshot via the secrets.resolve RPC method.

If you have questions about specific credential types, check out the AI Setup Assistant.

I’ve spent too much time worrying about plaintext secrets hiding in my configuration files. It is easy to leave a sensitive token in a .env file or an agent config without realizing it, which creates a security risk if those files are ever shared or committed. I want to show you the best way to clean this up using the OpenClaw secrets workflow.

  • OpenClaw CLI installed
  • Existing configuration files (like openclaw.json or auth-profiles.json)
  • Access to your terminal

You can fix your secret posture in about five minutes. I recommend following this basic loop to find and secure your credentials.

Terminal window
openclaw secrets audit --check
openclaw secrets configure
openclaw secrets audit --check

When you run secrets audit, the tool looks for several security issues. It specifically scans for:

  • Plaintext values at rest in openclaw.json, auth-profiles.json, .env, and generated agents/*/agent/models.json files.
  • Plaintext sensitive provider header residues in models.json entries.
  • Unresolved references.
  • Precedence shadowing, such as when auth-profiles.json takes priority over openclaw.json references.
  • Legacy residues like auth.json or OAuth reminders.

If you are wondering how it finds headers, it uses a name-heuristic. It looks for common credential fragments like authorization, x-api-key, token, secret, password, and credential.

The secrets configure command is an interactive tool that guides you through the setup. It starts by configuring your secrets.providers (supporting env, file, or exec).

From there, you can select secret-bearing fields in openclaw.json or auth-profiles.json. The tool captures SecretRef details including the source, provider, and id. Before it saves anything, it runs a preflight resolution to make sure the secrets actually work.

You can use these specific modes depending on what you need:

  • openclaw secrets configure --providers-only
  • openclaw secrets configure --skip-provider-setup
  • openclaw secrets configure --agent <id>

When you apply these changes, the CLI automatically scrubs matching static credentials from auth-profiles.json, legacy api_key entries from auth.json, and known secret lines from your .env file.

If you prefer to generate a plan first, you can apply it later using the secrets apply command. I suggest using the --dry-run flag first to see exactly what will happen.

Terminal window
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run

OpenClaw uses a one-way safety policy. It does not write rollback backups that contain historical plaintext secret values. To keep your data safe, the tool requires preflight checks to succeed before writing, and it uses atomic file replacement to update your configurations.

For those moving away from older setups, the runtime no longer depends on plaintext legacy auth storage. It uses an in-memory snapshot for credentials. While legacy api_key entries are scrubbed, OAuth-related compatibility behavior stays separate.

The Web UI form is hard to use for complex secrets Some SecretInput unions are easier to configure in raw editor mode than in form mode. If the UI feels restrictive, switch to the raw editor.

Audit reports precedence shadowing This happens when auth-profiles.json is taking priority over your openclaw.json references. Use the audit results to identify which file is winning and update your configuration to point to the correct SecretRef.

Need more help with your setup? Check out the AI Setup Assistant.

OpenClaw

OpenClaw Expert

Still stuck?

If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.