#Openclaw Secrets Audit

1 messages · Page 1 of 1 (latest)

median lotus
#

Results:

Secrets audit: findings. plaintext=8, unresolved=0, shadowed=0, legacy=1.
- [PLAINTEXT_FOUND] /root/.openclaw/openclaw.json:gateway.auth.token gateway.auth.token is stored as plaintext.
- [PLAINTEXT_FOUND] /root/.openclaw/openclaw.json:channels.discord.accounts.krill.token channels.discord.accounts.krill.token is stored as plaintext.
- [LEGACY_RESIDUE] /root/.openclaw/agents/main/agent/auth-profiles.json:profiles.openai-codex:default OAuth credentials are present (out of scope for static SecretRef migration).
- [PLAINTEXT_FOUND] /root/.openclaw/agents/main/agent/models.json:providers.codex.apiKey models.json provider apiKey is stored as plaintext.
- [PLAINTEXT_FOUND] /root/.openclaw/agents/krill/agent/models.json:providers.codex.apiKey models.json provider apiKey is stored as plaintext.
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.BRAVE_API_KEY Potential secret found in .env (BRAVE_API_KEY).
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.GOOGLE_API_KEY Potential secret found in .env (GOOGLE_API_KEY).
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.OPENROUTER_API_KEY Potential secret found in .env (OPENROUTER_API_KEY).
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.TAVILY_API_KEY Potential secret found in .env (TAVILY_API_KEY).

OpenClaw 2026.4.15 on Ubuntu with 2 agents configured. Model is gpt-5.4.

What next?

dire scrollBOT
raw shellBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> ✅ Mark Solution

dire scrollBOT
tough surgeBOT
#

Next step is to migrate the supported plaintext fields to SecretRefs, then rerun the audit.

I’d use the built-in flow:

openclaw secrets configure --plan-out /tmp/openclaw-secrets-plan.json
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json
openclaw secrets audit --check
openclaw secrets reload

For your findings specifically:

  • gateway.auth.token, channels.discord.accounts.krill.token, and models.providers.*.apiKey are supported SecretRef targets.
  • Don’t edit agents/*/agent/models.json directly. Those are generated residues, fix the source config instead.
  • The .env entries are still plaintext at rest. If you want the audit to go green, move those to SecretRefs backed by file/exec, or inject them outside ~/.openclaw/.env. If you keep them in .env, audit will keep flagging them.
#
  • The LEGACY_RESIDUE Codex OAuth entry is expected. OAuth material is intentionally out of scope for static SecretRef migration. Leave it if you still use it, otherwise revoke/remove it manually.

If you choose an exec provider, add --allow-exec on both apply commands.

Docs:
https://docs.openclaw.ai/cli/secrets
https://docs.openclaw.ai/gateway/secrets
https://docs.openclaw.ai/reference/secretref-credential-surface

If you want, paste the generated plan here and I can sanity-check it before you apply it.

median lotus
#
│  ● Add provider (Define a new env/file/exec provider)
│  ○ Continue
└
tough surgeBOT
#

At that prompt:

  • Choose Add provider if you want to fix the .env findings too.
  • Choose Continue only if those secrets already live in a real runtime env source, not ~/.openclaw/.env.

My recommendation here is:

  1. Add provider
  2. pick file if you want the easiest path on Ubuntu
    or exec if you already use 1Password/Vault/sops
  3. then map:
    • gateway.auth.token
    • channels.discord.accounts.krill.token
    • models.providers.codex.apiKey

Why: if you hit Continue now, you only get env refs. That cleans openclaw.json, but if the values still live in ~/.openclaw/.env, audit will still flag the .env secrets.

#

So the tradeoff is:

  • Continue / env refs: fastest, but only good if the secrets come from systemd/docker/env injection
  • Add provider / file: easiest full migration path
  • Add provider / exec: strongest, most setup

If you want the least painful route, do:

  • Add provider
  • file
  • path: /root/.openclaw/secrets.json
  • mode: json

Then continue mapping the three supported targets above, apply, and scrub the old .env entries.

If you want, send the next prompt after you pick file, and I’ll walk you through the exact values.

median lotus
#
│  Add provider
│
◇  Provider source
│  env
│
◆  Provider alias
│  default█
└

tough surgeBOT
#

Back up here, don’t add env/default.

That’s just the built-in env provider again, so it won’t help with the .env findings.

Use:

  • Provider source: file
  • Provider alias: localfile (or filemain)
  • Path: /root/.openclaw/secrets.json
  • Mode: json

So the move is:

  1. cancel/back out of this env provider
  2. add a new file provider
  3. continue from there
#

Tiny rule of thumb:

  • if you want env refs, you usually don’t need to add a provider
  • if you want to clean up ~/.openclaw/.env, add file or exec

Send the next screen after you switch to file, and I’ll give you the exact values.

median lotus
#

how do i back up from here?

tough surgeBOT
#

I don’t think that wizard has a real “go back” step yet.

Safest move:

  1. Press Ctrl+C
  2. it should cancel the secrets wizard
  3. rerun openclaw secrets configure

That’s safe here, configure is just building a plan until you actually apply it.

Then redo it with:

  • Add provider
  • source: file
  • alias: localfile
  • path: /root/.openclaw/secrets.json
  • mode: json
#

So short version: Ctrl+C, rerun, start again. No damage done.

median lotus
#

can you break it down on what exactly this does?

tough surgeBOT
#

Yep. Short version, this is not encryption, it’s moving secret lookup out of normal config.

What each part does:

1) secrets configure

This is just a planner.

It asks:

  • where secrets should live (env, file, or exec)
  • which config fields should stop storing plaintext
  • what SecretRef should replace each plaintext value

At this stage, it’s mostly building a plan. It does not immediately rotate secrets or send them anywhere.

2) file provider

If you choose a file provider like:

#
  • alias: localfile
  • path: /root/.openclaw/secrets.json
  • mode: json

you’re telling OpenClaw:

“When you need a secret, read it from this JSON file instead of storing it directly in openclaw.json.”

So instead of this:

gateway: {
  auth: {
    token: "actual-secret-here"
  }
}
#

you end up with something like:

gateway: {
  auth: {
    token: { source: "file", provider: "localfile", id: "/gateway/token" }
  }
}

And the real value lives in /root/.openclaw/secrets.json, for example:

{
  "gateway": {
    "token": "actual-secret-here"
#
  }
}

3) secrets apply

This is the part that actually writes changes.

It can:

  • replace plaintext config values with SecretRefs
  • scrub old plaintext from supported places
  • update generated model config so models.json stops carrying raw keys

It does not migrate the Codex OAuth residue, that one is intentionally out of scope.

4) secrets reload

This tells the running gateway:

#

“Reload secrets from the new refs and use the new runtime snapshot.”

So the live process starts using the SecretRefs instead of the old plaintext values.


Important caveat

A file provider still stores plaintext on disk, just in a dedicated secrets file instead of openclaw.json or .env.

So:

  • file = easiest cleanup, simpler ops
  • exec = stronger, because secrets can come from 1Password/Vault/sops without sitting in OpenClaw config files

For your specific findings

#

This flow helps with:

  • gateway.auth.token
  • channels.discord.accounts.krill.token
  • models.providers.codex.apiKey

For these .env ones:

  • BRAVE_API_KEY
  • GOOGLE_API_KEY
  • OPENROUTER_API_KEY
  • TAVILY_API_KEY

they will only stop showing up if you either:

  • move them to matching config fields backed by SecretRefs, or
  • remove them from .env if unused
#

If you want, I can do the next message as a literal before/after example for your exact 3 main fields.

median lotus
#

why cant we continue to use .env?

tough surgeBOT
#

You can continue to use .env.

That’s the important part. It’s not “forbidden”, it just means the audit will keep reporting those values as plaintext at rest.

The distinction is:

If you keep .env

You can still improve things by moving secrets out of:

  • openclaw.json
  • generated agents/*/agent/models.json

and into env-backed refs.

That gives you fewer plaintext copies.

But .env is still a plaintext file

#

So the audit will still say:

  • BRAVE_API_KEY found in .env
  • GOOGLE_API_KEY found in .env
  • etc.

So really there are 2 goals:

  1. Reduce duplicate secret copies
    .env is fine for this.

  2. Stop storing secrets in plaintext files on disk
    .env does not solve this, because .env itself is a plaintext file.


Honest recommendation

#

If your goal is practical cleanup with minimal pain, keep .env and migrate the config fields to env refs.

If your goal is make secrets audit as clean as possible, you need to move off .env for those keys too.

So I only pushed file/exec because you seemed to be aiming for a cleaner audit, not because .env is unusable.

If you want, I can give you the simplest .env-based plan next, which should cut your findings down without changing your overall setup much.

median lotus
#

explain the file way of doing this

tough surgeBOT
#

Sure. The file way means:

keep the real secrets in one dedicated file, and make openclaw.json store pointers to them instead of the raw values.

What that looks like

1) You add a file provider

Example:

  • alias: localfile
  • path: /root/.openclaw/secrets.json
  • mode: json

That tells OpenClaw:

“When a SecretRef says source: "file", provider: "localfile", read it from /root/.openclaw/secrets.json.”

#

2) Your real secrets live in that file

Example secrets.json:

{
  "gateway": {
    "token": "REAL_GATEWAY_TOKEN"
  },
  "discord": {
    "krillToken": "REAL_DISCORD_TOKEN"
  },
  "codex": {
    "apiKey": "REAL_CODEX_KEY"
#
  }
}

3) openclaw.json stops storing the raw secrets

Instead of this:

gateway: {
  auth: {
    token: "REAL_GATEWAY_TOKEN"
  }
#
}

you get this:

gateway: {
  auth: {
    token: { source: "file", provider: "localfile", id: "/gateway/token" }
  }
}

Same idea for the others:

#
channels: {
  discord: {
    accounts: {
      krill: {
        token: { source: "file", provider: "localfile", id: "/discord/krillToken" }
      }
    }
  }
}

models: {
  providers: {
    codex: {
      apiKey: { source: "file", provider: "localfile", id: "/codex/apiKey" }
    }
#
  }
}

The id is just a JSON pointer into secrets.json.


Why people do it

Pros

  • secrets stop living in openclaw.json
  • generated models.json should stop carrying plaintext copies
  • easier to centralize/backup one secret file
  • easier than setting up Vault/1Password/exec
#

Cons

  • it is still plaintext on disk
  • so it’s cleaner than raw config, but not as strong as an exec secret manager
  • if you want the strongest setup, exec is better

For your audit specifically

The file path would help with:

  • gateway.auth.token
  • channels.discord.accounts.krill.token
  • models.providers.codex.apiKey

It can also help with the .env keys if you move those secrets into config-backed SecretRefs too. Otherwise those .env findings remain.

#

Best mental model

  • env = “read secret from environment”
  • file = “read secret from a JSON file”
  • exec = “ask another program/secret manager for the secret”

If you want, I can write out the exact secrets.json shape I’d use for your case, with all 7 relevant secrets mapped cleanly.

median lotus
#

what confuses me is that, in both ways, the secret still lives in a file.

  • env : secret lives in `~/.openclaw/.env
  • file: secret lives in `~/.openclaw/secrets.json

so it seems there's no difference. Correct me if i am wrong.

median lotus
#

Krill?

tough surgeBOT
#

Yep, I’m here 🦐

Want me to do the exact secrets.json layout for your setup next, or the exact clicks/prompts in openclaw secrets configure?

median lotus
#

what confuses me is that, in both ways, the secret still lives in a file.
env : secret lives in ~/.openclaw/.env file: secret lives in ~/.openclaw/secrets.json

so it seems there's no difference. Correct me if i am wrong. Tell me what is the difference

tough surgeBOT
#

You’re basically right.

If both are:

  • plaintext
  • on the same machine
  • with similar file permissions

then ~/.openclaw/.env and ~/.openclaw/secrets.json are not meaningfully different for “secret sitting on disk” risk.

That’s the honest answer.

So what is the difference?

1) Audit semantics

OpenClaw treats these differently:

#
  • .env = part of the app’s plaintext config surface, so audit flags it
  • file provider = an external secret source that refs point into

So yes, this is partly an audit/modeling difference, not magic security.

2) Separation of concerns

With file:

  • openclaw.json contains refs
  • secrets.json contains values

That means you can copy/share/back up config more safely without dragging the secret values around in the same file.

With .env, secrets are still in a general-purpose env file that a lot of tooling treats as ambient config.

3) Blast radius

Env vars are process-wide inputs. They tend to be more “ambient”.

#

A file provider is more explicit:

  • OpenClaw resolves the refs it needs
  • values are structured
  • you are not relying on broad env fallback behavior

That’s cleaner operationally, even if it is still plaintext.


The real security ranking is more like this

  • .env: simple, plaintext on disk
  • file provider: cleaner separation, still plaintext on disk
  • exec provider / Vault / 1Password / sops / system secret manager: actual improvement in secret handling

So if your question is:

#

“Is file actually much more secure than .env if both are plaintext files?”

No, not really.

If your question is:

“Why would someone still use file?”

Because it gives:

  • cleaner config
  • explicit refs
  • less secret duplication in OpenClaw-managed files
  • better fit for the SecretRef model
  • cleaner audit behavior

My recommendation for your case

#
  • If you want least friction, keep .env and move the config fields to env refs.
  • If you want cleaner config/audit, use file.
  • If you want real security improvement, use exec or a proper secret manager.

So your intuition is good here. The big jump is not .envfile. The big jump is .env/fileexternal secret manager.

median lotus
#

ok, i will use file

#

guide me

tough surgeBOT
#

Nice. I’d honestly skip the wizard now and do the file setup explicitly.

1) Create the secrets file

/root/.openclaw/secrets.json

Example:

{
  "gateway": {
    "token": "YOUR_GATEWAY_TOKEN"
  },
  "discord": {
    "krill": {
      "token": "YOUR_DISCORD_TOKEN"
    }
#
  },
  "models": {
    "codex": {
      "apiKey": "YOUR_CODEX_KEY"
    }
  },
  "web": {
    "brave": {
      "apiKey": "YOUR_BRAVE_KEY"
    },
    "google": {
      "apiKey": "YOUR_GOOGLE_KEY"
    },
    "tavily": {
      "apiKey": "YOUR_TAVILY_KEY"
#
    }
  }
}

2) Register the file provider

openclaw config set secrets.providers.localfile \
  --provider-source file \
  --provider-path /root/.openclaw/secrets.json \
  --provider-mode json

3) Point config at that file

#
openclaw config set gateway.auth.token \
  --ref-provider localfile --ref-source file --ref-id /gateway/token

openclaw config set channels.discord.accounts.krill.token \
  --ref-provider localfile --ref-source file --ref-id /discord/krill/token

openclaw config set models.providers.codex.apiKey \
  --ref-provider localfile --ref-source file --ref-id /models/codex/apiKey

4) If you want to clear the .env findings too

Map the ones you actually use, then delete those lines from ~/.openclaw/.env.

Examples:

#
openclaw config set plugins.entries.brave.config.webSearch.apiKey \
  --ref-provider localfile --ref-source file --ref-id /web/brave/apiKey

openclaw config set plugins.entries.google.config.webSearch.apiKey \
  --ref-provider localfile --ref-source file --ref-id /web/google/apiKey

openclaw config set plugins.entries.tavily.config.webSearch.apiKey \
  --ref-provider localfile --ref-source file --ref-id /web/tavily/apiKey

For OPENROUTER_API_KEY, pause a sec because that one depends on what you use it for:

  • Perplexity web searchplugins.entries.perplexity.config.webSearch.apiKey
  • model provider auth → move it to the matching model/auth config instead

5) Reload and verify

#
openclaw secrets reload
openclaw secrets audit --check

6) Expected result

  • openclaw.json plaintext findings should disappear
  • generated agents/*/agent/models.json Codex plaintext should disappear after reload/apply path catches up
  • the legacy Codex OAuth residue will still remain unless you remove/revoke it manually

Docs:
https://docs.openclaw.ai/cli/secrets
https://docs.openclaw.ai/gateway/secrets
https://docs.openclaw.ai/reference/secretref-credential-surface

If you want, paste how you use OPENROUTER_API_KEY and I’ll tell you the exact correct target path for that one.

#

Yep. Let’s do the smallest safe file migration first.

0) First, verify what is actually plaintext

Run these:

openclaw --version
openclaw config get gateway.auth.token --json
openclaw config get channels.discord.accounts.krill.token --json
openclaw config get models.providers.codex.apiKey --json

If the first two come back as plaintext strings, migrate them.

For models.providers.codex.apiKey, only migrate it if that also comes back as a plaintext string.
If it comes back empty or already looks like a ref object, stop and paste it here.

#

1) Create the secrets file

Create /root/.openclaw/secrets.json with this shape:

{
  "gateway": {
    "token": "YOUR_GATEWAY_TOKEN"
  },
  "discord": {
    "krill": {
      "token": "YOUR_DISCORD_TOKEN"
    }
  },
  "models": {
#
    "codex": {
      "apiKey": "YOUR_CODEX_API_KEY"
    }
  }
}

Then lock it down a bit:

chmod 600 /root/.openclaw/secrets.json

2) Register the file provider

#
openclaw config set secrets.providers.localfile \
  --provider-source file \
  --provider-path /root/.openclaw/secrets.json \
  --provider-mode json

3) Point config at the file

openclaw config set gateway.auth.token \
  --ref-provider localfile --ref-source file --ref-id /gateway/token

openclaw config set channels.discord.accounts.krill.token \
#
  --ref-provider localfile --ref-source file --ref-id /discord/krill/token

If models.providers.codex.apiKey was plaintext in step 0, also run:

openclaw config set models.providers.codex.apiKey \
  --ref-provider localfile --ref-source file --ref-id /models/codex/apiKey

4) Reload and re-audit

#
openclaw secrets reload
openclaw secrets audit --check

5) What to expect

After this:

  • gateway.auth.token plaintext finding should go away
  • channels.discord.accounts.krill.token plaintext finding should go away
  • the models.json Codex finding should usually clear once the source is fixed and the runtime regenerates
  • the legacy OAuth residue will still remain, that part is expected
  • the .env findings will still remain for now

So don’t touch .env yet. First get the core 2 or 3 refs working.

median lotus
#

I don't have Codex key. My code is Oauth.

🦞 OpenClaw 2026.4.15 (041266a) — Open source means you can see exactly how I judge your config.

│
◇
Secrets reloaded with 2 warning(s).

🦞 OpenClaw 2026.4.15 (041266a) — Greetings, Professor Falken

Secrets audit: findings. plaintext=6, unresolved=0, shadowed=0, legacy=1.
- [LEGACY_RESIDUE] /root/.openclaw/agents/main/agent/auth-profiles.json:profiles.openai-codex:default OAuth credentials are present (out of scope for static SecretRef migration).
- [PLAINTEXT_FOUND] /root/.openclaw/agents/main/agent/models.json:providers.codex.apiKey models.json provider apiKey is stored as plaintext.
- [PLAINTEXT_FOUND] /root/.openclaw/agents/krill/agent/models.json:providers.codex.apiKey models.json provider apiKey is stored as plaintext.
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.BRAVE_API_KEY Potential secret found in .env (BRAVE_API_KEY).
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.GOOGLE_API_KEY Potential secret found in .env (GOOGLE_API_KEY).
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.OPENROUTER_API_KEY Potential secret found in .env (OPENROUTER_API_KEY).
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.TAVILY_API_KEY Potential secret found in .env (TAVILY_API_KEY).
root@vmi3151434:~#```
#

what's next krill?

#

Krill?

tough surgeBOT
#

Nice, that narrows it down.

  • gateway.auth.token and the Discord token are fixed now.
  • profiles.openai-codex:default is your OAuth login, so that LEGACY_RESIDUE is expected and not something SecretRefs migrate.
  • Since you do not have a Codex API key, do not try to create a ref for providers.codex.apiKey.

The important split is:

  • openai-codex/* = ChatGPT/Codex OAuth
  • codex/* = separate Codex harness/app-server route

Your remaining models.json findings are for providers.codex.apiKey, so first check whether that is active config or just stale generated residue.

Run these and paste the output:

#
openclaw config get agents.defaults.model.primary --json
openclaw config get models.providers.codex --json
openclaw config get models.providers.openai-codex --json

How to read it:

  • if primary model is openai-codex/gpt-5.4, you’re on the OAuth path
  • if primary model is codex/gpt-5.4, you’re on the Codex harness path
  • if models.providers.codex is empty but audit still reports /root/.openclaw/agents/*/agent/models.json:providers.codex.apiKey, that looks like stale generated residue on 2026.4.15, and I would not edit agents/*/agent/models.json directly

So the remaining real plaintext-at-rest items are your .env keys:

  • BRAVE_API_KEY
  • GOOGLE_API_KEY
  • OPENROUTER_API_KEY
#

You’re close 🦐

What’s next

1) Ignore this one for now

This is expected with your setup:

  • /root/.openclaw/agents/main/agent/auth-profiles.json:profiles.openai-codex:default

That’s your OAuth credential, and the docs/source treat it as out of scope for static SecretRef migration.

2) Check the codex vs openai-codex mismatch

Since you said “I don’t have Codex key, my code is OAuth”, the remaining providers.codex.apiKey findings are probably stale codex provider residue, not something you should migrate.

Run:

#
openclaw config get agents.defaults.model.primary --json
openclaw config get models.providers.codex --json
openclaw config get models.providers.openai-codex --json

What I’m expecting:

  • OAuth path = openai-codex/*
  • Codex harness path = codex/*

If models.providers.codex prints a real config object and you are not intentionally using the Codex harness, remove that stale provider block:

openclaw config unset models.providers.codex
openclaw secrets reload
#
openclaw secrets audit --check

If the two agents/*/agent/models.json:providers.codex.apiKey findings still remain after that, restart once so the generated files get refreshed:

openclaw gateway restart
openclaw secrets audit --check

And important, don’t edit agents/*/agent/models.json directly.

3) Then decide what to do with the 4 .env secrets

These are now your remaining real plaintext-at-rest items:

  • BRAVE_API_KEY
#
  • GOOGLE_API_KEY
  • OPENROUTER_API_KEY
  • TAVILY_API_KEY

Two valid choices:

  • Keep .env and accept those audit warnings
  • Move them to your file provider and replace supported config fields with SecretRefs

If you already set up localfile, the safest next move is:

openclaw secrets configure --skip-provider-setup

Then map the remaining supported credentials to that existing file provider, apply, then:

#

Yep, I’m here 🦐

You already cleared the real top-level plaintext stuff. What’s left is basically 3 buckets:

1) Expected, leave it alone

  • /root/.openclaw/agents/main/agent/auth-profiles.json:profiles.openai-codex:default

That’s your Codex OAuth login. Docs/source treat it as out of scope for static SecretRef migration.

2) Needs one check

These two:

  • /root/.openclaw/agents/main/agent/models.json:providers.codex.apiKey
  • /root/.openclaw/agents/krill/agent/models.json:providers.codex.apiKey

Since you said you use OAuth and do not have a Codex API key, do not make up a codex key ref.

#

Run this:

openclaw config get agents.defaults.model.primary --json
openclaw config get models.providers.codex --json
openclaw config get models.providers.openai-codex --json

If models.providers.codex is empty or null, it’s probably stale generated residue.

If models.providers.codex is a real block, and you are not intentionally using the Codex harness, the next move is:

openclaw config unset models.providers.codex
openclaw secrets reload
openclaw secrets audit --check
median lotus
#
"openai-codex/gpt-5.4"
Config path not found: models.providers.codex
Config path not found: models.providers.openai-codex```
tough surgeBOT
#

Perfect, that tells us a lot.

You are on Codex OAuth:

  • primary model = "openai-codex/gpt-5.4"
  • models.providers.codex = not configured
  • models.providers.openai-codex = not configured

So those 2 remaining findings:

  • /root/.openclaw/agents/main/agent/models.json:providers.codex.apiKey
  • /root/.openclaw/agents/krill/agent/models.json:providers.codex.apiKey

are almost certainly stale generated models.json residue, not an active config value you need to migrate.

Best next move

#

Force OpenClaw to fully rewrite models.json, then re-audit.

pnpm openclaw config set models.mode '"replace"' --strict-json
pnpm openclaw gateway restart
pnpm openclaw secrets audit --check

Why: the docs say models.mode: "replace" is what you use when you want config to fully rewrite models.json. Right now you likely have old codex provider residue hanging around from merge behavior.

What should remain after that

Probably only:

  • the OAuth residue in /root/.openclaw/agents/main/agent/auth-profiles.json:profiles.openai-codex:default, which is expected
  • the 4 .env findings:
    • BRAVE_API_KEY
#
  • GOOGLE_API_KEY
    • OPENROUTER_API_KEY
    • TAVILY_API_KEY

Then final choice

For those .env findings, you have 2 honest options:

  1. keep .env, accept audit warnings
  2. move them to your file provider, if you want a cleaner audit

So I’d do this in order:

  1. rewrite models.json with models.mode: "replace"
  2. rerun audit
  3. if only .env + OAuth remain, decide whether you want to migrate the 4 .env keys

Docs: https://docs.openclaw.ai/gateway/configuration-reference https://docs.openclaw.ai/gateway/secrets

#

If you paste the next secrets audit --check, I’ll tell you the exact final cleanup step.

median lotus
#
pnpm openclaw gateway restart
pnpm openclaw secrets audit --check
Command 'pnpm' not found, did you mean:
  command 'npm' from deb npm (9.2.0~ds1-2)
Try: apt install <deb name>
Command 'pnpm' not found, did you mean:
  command 'npm' from deb npm (9.2.0~ds1-2)
Try: apt install <deb name>
Command 'pnpm' not found, did you mean:
  command 'npm' from deb npm (9.2.0~ds1-2)
Try: apt install <deb name>
tough surgeBOT
#

My bad, that wrapper was wrong for your install.

Since openclaw already works on your box, use the CLI directly, not pnpm openclaw.

Run this instead:

openclaw config set models.mode replace
openclaw gateway restart
openclaw secrets audit --check

If you want to verify the setting stuck first:

openclaw config get models.mode --json
median lotus
#
Secrets audit: findings. plaintext=6, unresolved=0, shadowed=0, legacy=1.
- [LEGACY_RESIDUE] /root/.openclaw/agents/main/agent/auth-profiles.json:profiles.openai-codex:default OAuth credentials are present (out of scope for static SecretRef migration).
- [PLAINTEXT_FOUND] /root/.openclaw/agents/main/agent/models.json:providers.codex.apiKey models.json provider apiKey is stored as plaintext.
- [PLAINTEXT_FOUND] /root/.openclaw/agents/krill/agent/models.json:providers.codex.apiKey models.json provider apiKey is stored as plaintext.
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.BRAVE_API_KEY Potential secret found in .env (BRAVE_API_KEY).
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.GOOGLE_API_KEY Potential secret found in .env (GOOGLE_API_KEY).
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.OPENROUTER_API_KEY Potential secret found in .env (OPENROUTER_API_KEY).
- [PLAINTEXT_FOUND] /root/.openclaw/.env:$env.TAVILY_API_KEY Potential secret found in .env (TAVILY_API_KEY).
tough surgeBOT
#

Good news, you’ve basically isolated the real leftovers.

What your current audit means

Expected, leave it alone

  • /root/.openclaw/agents/main/agent/auth-profiles.json:profiles.openai-codex:default

That is your OpenAI Codex OAuth credential. It is explicitly out of scope for static SecretRef migration.

Real plaintext, but by choice

These are still real plaintext-at-rest because they live in .env:

  • BRAVE_API_KEY
  • GOOGLE_API_KEY
  • OPENROUTER_API_KEY
  • TAVILY_API_KEY
#

If they stay in /root/.openclaw/.env, audit will keep flagging them.

Weird one

These two are the odd part:

  • /root/.openclaw/agents/main/agent/models.json:providers.codex.apiKey
  • /root/.openclaw/agents/krill/agent/models.json:providers.codex.apiKey

Because you already confirmed:

  • primary model is openai-codex/gpt-5.4
  • models.providers.codex is not configured
  • models.providers.openai-codex is not configured

So I’m pretty confident those two are generated residue / a 2026.4.15 quirk, not an active secret you still need to migrate.

My recommendation

#
  1. Do not create a fake Codex key ref
  2. Do not edit agents/*/agent/models.json directly
  3. Treat the remaining real cleanup as the .env keys
  4. For the stubborn providers.codex.apiKey findings, run one diagnostic pass:
openclaw doctor
openclaw status --deep

If doctor/status do not show any active codex provider config, I would treat those audit lines as a likely bug/residue on 2026.4.15 and file an issue with the exact evidence:

  • primary model: openai-codex/gpt-5.4
  • models.providers.codex not found
  • models.providers.openai-codex not found
  • audit still reports agents/*/agent/models.json:providers.codex.apiKey
#

GitHub issues: https://github.com/openclaw/openclaw/issues

So, what’s next next?

The real decision is:

  • stop here, and accept .env + OAuth + likely residue warnings, or
  • migrate the 4 .env keys to your file provider for a cleaner audit

If you want, I can do the exact next step for those four .env keys, with the exact secrets.json layout and which config paths each one should move to.

median lotus
#

migrate the 4 .env keys to your file provider for a cleaner audit. Do note that, I am using

  • GOOGLE_API_KEY for accessing gemini models and not for web search
  • OPENROUTER_API_KEY for Openrouter models
  • BRAVE_API_KEY: Since I am using Tavily, I am not using Brave anymore
#
  • TAVILY_API_KEY: already migrated
tough surgeBOT
#

Yep. Given your usage, I’d do 3 migrations + 1 delete.

Map them like this

  • GOOGLE_API_KEYmodels.providers.google.apiKey
  • OPENROUTER_API_KEYmodels.providers.openrouter.apiKey
  • TAVILY_API_KEYplugins.entries.tavily.config.webSearch.apiKey
  • BRAVE_API_KEYdelete it from /root/.openclaw/.env since you’re not using Brave anymore

And importantly, do not set plugins.entries.google.config.webSearch.apiKey, since your Google key is for Gemini models, not web search.


1) Make sure your file provider exists

#
openclaw config get secrets.providers.localfile --json

If that says path not found, create it:

openclaw config set secrets.providers.localfile \
  --provider-source file \
  --provider-path /root/.openclaw/secrets.json \
  --provider-mode json

2) Put only the active secrets into /root/.openclaw/secrets.json

#
{
  "models": {
    "google": {
      "apiKey": "YOUR_GOOGLE_API_KEY"
    },
    "openrouter": {
      "apiKey": "YOUR_OPENROUTER_API_KEY"
    }
  },
  "web": {
    "tavily": {
      "apiKey": "YOUR_TAVILY_API_KEY"
    }
  }
}
#

Then lock it down:

chmod 600 /root/.openclaw/secrets.json

3) Point config at that file

openclaw config set models.providers.google.apiKey \
  --ref-provider localfile --ref-source file --ref-id /models/google/apiKey

openclaw config set models.providers.openrouter.apiKey \
  --ref-provider localfile --ref-source file --ref-id /models/openrouter/apiKey
#

openclaw config set plugins.entries.tavily.config.webSearch.apiKey \
  --ref-provider localfile --ref-source file --ref-id /web/tavily/apiKey

4) Clean up /root/.openclaw/.env

After the refs are set, remove these lines from /root/.openclaw/.env:

  • GOOGLE_API_KEY=...
  • OPENROUTER_API_KEY=...
  • TAVILY_API_KEY=...
  • BRAVE_API_KEY=...
#

I’d keep a temporary backup copy until the audit is clean.


5) Reload and re-audit

openclaw secrets reload
openclaw gateway restart
openclaw secrets audit --check

What should remain after this

Expected:

#

Perfect, then it’s 2 migrations + 2 deletes now.

Do this next

Add refs for the two still-active .env keys

openclaw config set models.providers.google.apiKey \
  --ref-provider localfile --ref-source file --ref-id /models/google/apiKey

openclaw config set models.providers.openrouter.apiKey \
  --ref-provider localfile --ref-source file --ref-id /models/openrouter/apiKey

Make sure /root/.openclaw/secrets.json contains those values:

#
{
  "models": {
    "google": {
      "apiKey": "YOUR_GOOGLE_API_KEY"
    },
    "openrouter": {
      "apiKey": "YOUR_OPENROUTER_API_KEY"
    }
  }
}

If your Tavily secret is already in there, keep it.

Then clean up /root/.openclaw/.env

Remove these lines from .env:

#
  • GOOGLE_API_KEY=...
  • OPENROUTER_API_KEY=...
  • TAVILY_API_KEY=... ← since you already migrated it
  • BRAVE_API_KEY=... ← since you no longer use Brave

Reload and re-audit

openclaw secrets reload
openclaw gateway restart
openclaw secrets audit --check

What I expect after that

The .env findings should disappear.

You’ll probably still see:

median lotus
#
🦞 OpenClaw 2026.4.15 (041266a) — Greetings, Professor Falken

Error: Config validation failed: models.providers.google.baseUrl: Invalid input: expected string, received undefined

🦞 OpenClaw 2026.4.15 (041266a) — I keep secrets like a vault... unless you print them in debug logs again.

Error: Config validation failed: models.providers.openrouter.baseUrl: Invalid input: expected string, received undefined```
tough surgeBOT
#

Yep, that error is actually useful.

models.providers.google and models.providers.openrouter are being treated as custom provider blocks, so setting only .apiKey creates an invalid half-config, which is why it complains about missing baseUrl.

So for your case, the fix is:

  • do not use models.providers.google.apiKey
  • do not use models.providers.openrouter.apiKey
  • use auth-profile refs for Google and OpenRouter instead

What to do next

1) Keep Tavily as-is, delete Brave

Since TAVILY_API_KEY is already migrated, and you do not use Brave anymore:

remove these from /root/.openclaw/.env later:

#
  • TAVILY_API_KEY=...
  • BRAVE_API_KEY=...

2) Create file-backed auth refs for Google + OpenRouter

If your localfile provider already exists, run:

openclaw secrets configure --skip-provider-setup --agent main

In the wizard:

  • create a new auth-profile mapping
  • provider: google
  • mode: api_key
  • profile id: google:default (or similar)
  • ref source: file
#
  • ref provider: localfile
  • ref id: /models/google/apiKey

Then create another one:

  • provider: openrouter
  • mode: api_key
  • profile id: openrouter:default
  • ref source: file
  • ref provider: localfile
  • ref id: /models/openrouter/apiKey

If your krill agent also needs Google/OpenRouter models, repeat for that agent too:

openclaw secrets configure --skip-provider-setup --agent krill
#

3) Make sure secrets.json has those values

It should look roughly like:

{
  "models": {
    "google": {
      "apiKey": "YOUR_GOOGLE_API_KEY"
    },
    "openrouter": {
      "apiKey": "YOUR_OPENROUTER_API_KEY"
    }
  },
  "web": {
    "tavily": {
      "apiKey": "YOUR_TAVILY_API_KEY"
#
    }
  }
}

4) Then remove the old .env lines

After the refs are in place, remove from /root/.openclaw/.env:

  • GOOGLE_API_KEY=...
  • OPENROUTER_API_KEY=...
  • TAVILY_API_KEY=...
  • BRAVE_API_KEY=...

5) Reload and verify

#
openclaw secrets reload
openclaw gateway restart
openclaw models status --agent main
openclaw secrets audit --check

If you also mapped krill, check that too:

openclaw models status --agent krill

What should remain after this

Probably only:

  • the expected OAuth residue:
median lotus
#

this look over complicated for the model providers. It means every time i create a new agent, i need to create the secrets.json for it and fill it up with the keys. Then if i add a new model provider (eg. anthropic), i will need to add its key to all the secrets.json in each agent. Is there another way?

tough surgeBOT
#

Yeah, I agree, that’s too much for model providers.

Short answer

Yes, there is another way.
For model provider keys, the low-friction option is to use a shared gateway environment, not per-agent file refs.

Important clarification

You do not need one secrets.json per agent.

If you use the file provider, you can keep one shared file, for example:

  • /root/.openclaw/secrets.json

All agents can point at the same file.

#

The annoying part is not the file, it’s that auth profiles are per-agent. The docs are explicit about that, and if you want to share creds, you copy auth-profiles.json into the other agent’s agentDir.

Best practical setup for your case

I’d split it like this:

Keep as shared/global

  • TAVILY_API_KEY via your existing shared file provider
  • delete BRAVE_API_KEY

For model providers, use shared env/service env

  • GOOGLE_API_KEY
  • OPENROUTER_API_KEY

That way:

  • every agent can use them
#
  • you do not need to create per-agent auth refs for every new agent
  • adding Anthropic later is just adding one more env var globally

Tradeoff

  • Easiest ops: gateway/service environment
  • Cleanest OpenClaw-native secret modeling: per-agent auth-profile refs

For model providers, I’d honestly recommend easiest ops unless you specifically want different creds per agent.

So my recommendation

  1. Leave Tavily migrated
  2. Delete Brave
  3. Put GOOGLE_API_KEY and OPENROUTER_API_KEY in the gateway process environment / service environment
  4. Remove those keys from ~/.openclaw/.env if you want the audit quieter
  5. Only use per-agent auth-profile refs when you actually need agent-specific credentials
median lotus
#

i am done with this, it's too complicated