#Control UI says invalid config + MissingEnvVarError for NVIDIA_API_KEY (DO 1‑click, 2026.2.12)

1 messages · Page 1 of 1 (latest)

flat nymphBOT
#

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

rustic ridgeBOT
#

That behavior is basically “service env vs interactive shell env”.

  • Your systemd service (openclaw.service) runs with EnvironmentFiles=/opt/openclaw.env, so the gateway process gets NVIDIA_API_KEY from systemd and can start fine.
  • When you do sudo -iu openclaw and run openclaw --version, your login shell does not automatically load /opt/openclaw.env, so ${NVIDIA_API_KEY} substitution in /home/openclaw/.openclaw/openclaw.json fails → MissingEnvVarError.

Docs that explain the precedence:
https://docs.openclaw.ai/help/environment (and FAQ section https://docs.openclaw.ai/help/faq#env-vars-and-env-loading)

Fix options

Option A (quick test): source the env file before running the CLI

sudo -iu openclaw
set -a
source /opt/openclaw.env
set +a
#
openclaw --version

Option B (recommended): put the key in the global OpenClaw .env for the openclaw user
OpenClaw auto-loads $OPENCLAW_STATE_DIR/.env (by default ~/.openclaw/.env, but in your case it looks like the state dir is ~/.openclaw or ~/.openclaw-equivalent at /home/openclaw/.openclaw since that’s where your config lives).
So create:

  • /home/openclaw/.openclaw/.env and add:
    NVIDIA_API_KEY=...
    

Option C: remove that provider config / env-var reference if you don’t actually use NVIDIA (because config parsing will try to resolve ${…} even if you never select that provider).

Sanity checks (if you want to confirm what the service sees)

#
pid=$(systemctl show -p MainPID --value openclaw.service)
sudo tr '\0' '\n' < /proc/$pid/environ | grep NVIDIA_API_KEY

If this fixed it, please right-click the solution message → Apps → ✅ Mark Solution so Answer Overflow indexes it.

elder topaz
#

generate a context file for what you understood about my openclaw setup.. so that i can attach that context for other issues i will ask here... remove the issue specific information.. but keep the generic context that i can reuse for my future issues

rustic ridgeBOT
#

Here’s a reusable context file you can copy/paste into future #help threads. I stripped out the issue-specific error details and kept the stable setup facts.

# OpenClaw setup context (VPS / systemd)

## Host / environment
- Type: Linux VPS (DigitalOcean 1-click style image)
- Init system: systemd
- Shell access: root available
- Dedicated service user: `openclaw`

## OpenClaw install
- CLI binary: `/usr/bin/openclaw`
- Node module location: `/usr/lib/node_modules/openclaw` (looks like a global install)
- OpenClaw version: `2026.2.12` (update this when you upgrade)
#
## Gateway service
- systemd unit name: `openclaw.service`
- Unit file: `/etc/systemd/system/openclaw.service`
- Runs as user: `openclaw`
- ExecStart: `/usr/bin/openclaw gateway ...`
- EnvironmentFile used by systemd: `/opt/openclaw.env` (contains API keys / env vars)

## Config / state paths
- Config path: `/home/openclaw/.openclaw/openclaw.json` (JSON5 config)
- State dir appears to be: `/home/openclaw/.openclaw/`
  - Note: you can also place a global OpenClaw env file here at:
    `/home/openclaw/.openclaw/.env` (if you want env vars available outside systemd too)

## Env var loading model (important operational detail)
- The **systemd service** loads env vars from `/opt/openclaw.env`.
#
- An **interactive shell** (e.g. `sudo -iu openclaw`) does *not* automatically load `/opt/openclaw.env`
  unless you manually `source` it.
- OpenClaw also auto-loads `.env` files (non-overriding):
  - `.env` in current working directory
  - global fallback `.env` in the OpenClaw state dir (`~/.openclaw/.env` / `$OPENCLAW_STATE_DIR/.env`)
- Docs: https://docs.openclaw.ai/help/environment

## Logs / debugging commands I can paste (no secrets)
- Service status:
  - `systemctl status openclaw.service`
  - `systemctl show openclaw.service -p User -p ExecStart -p EnvironmentFiles -p FragmentPath`
- Logs:
  - `journalctl -u openclaw.service -n 200 --no-pager`
  - `journalctl -u openclaw.service -f`
- OpenClaw diagnostics:
#
  - `openclaw status`
  - `openclaw models status`
  - `openclaw doctor`
  - `openclaw health --verbose`

## Redaction rule
- Never paste API keys/tokens.
- If you need to show configs, replace secrets with `<REDACTED>` and keep only structure/paths.

If you tell me your distro (cat /etc/os-release) and whether you’re binding the gateway to localhost/lan/tailnet, I can add those as optional extra bullets in the same template.

elder topaz
#

root@openclaw3:~# cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.4 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo

what you mean "whether you’re binding the gateway to localhost/lan/tailnet,"

rustic ridgeBOT
#

Those are just the common gateway network bind modes people use, i.e. what interface/address the Gateway listens on, and therefore who can reach it.

In OpenClaw that’s controlled by the config key:

  • gateway.bind

Typical values you’ll see people use:

  • "localhost" (or default-ish): listens only on 127.0.0.1 → only the same machine can access the Dashboard/Gateway directly.
  • "lan": listens on the machine’s LAN/private interface (e.g. 10.x/192.168.x) → other machines on your network (or VPC) can connect.
  • "tailnet": binds to the Tailscale interface (tailscale0) → only devices on your Tailnet can connect.

Why I asked: when debugging “can’t connect”, “unauthorized”, “Control UI can’t reach gateway”, etc., the bind mode is often the culprit.

How to check yours quickly

From config:

#
sudo -iu openclaw
grep -n "gateway" -n /home/openclaw/.openclaw/openclaw.json | head

Better if you can share just the gateway block (redacting secrets).

From the running system (ports/listeners):

sudo ss -ltnp | grep -E '(:3000|openclaw|node)' || true

From OpenClaw (if the CLI works in that env):

openclaw status
openclaw gateway status
#

If you paste your gateway: section (with tokens/passwords redacted), I’ll tell you exactly which mode you’re on and what it implies.

elder topaz
#

/opt/openclaw.env > OPENCLAW_GATEWAY_BIND=lan

rustic ridgeBOT
#

Perfect — that answers it.

OPENCLAW_GATEWAY_BIND=lan means your gateway is configured to bind on the machine’s LAN/private network interfaces, so other machines in the same private network (VPC / subnet) can reach it.

A few practical implications:

  • The gateway will not be “localhost-only”; it should listen on something like 0.0.0.0 (or the droplet’s private IP).
  • On a VPS, “lan” usually means the private/VPC interface, not necessarily the public internet — but this depends on firewall/cloud rules.
  • You still need the firewall/security group to allow inbound traffic on the gateway port (whatever you’re using).

If you want to confirm what it’s actually listening on + port, run:

sudo ss -ltnp | grep -i openclaw
# or just:
sudo ss -ltnp | head
elder topaz
#

i think, let's leave other details for now.. i will add them later if any specific issue need it

rustic ridgeBOT
#

Yep, that’s a good approach.

For future threads, the minimal reusable context you already have is basically:

  • Ubuntu 24.04 (Noble) VPS
  • OpenClaw 2026.2.12
  • systemd service openclaw.service runs as user openclaw
  • config at /home/openclaw/.openclaw/openclaw.json
  • systemd env file /opt/openclaw.env
  • OPENCLAW_GATEWAY_BIND=lan

Then only add extra stuff (ports, ss -ltnp, openclaw status, gateway logs, etc.) when a specific issue needs it.

If this thread is resolved, please right-click the solution message → Apps → ✅ Mark Solution so it gets indexed on Answer Overflow.

elder topaz
#

you sure - systemd service openclaw.service runs as user openclaw and not as 'root`

rustic ridgeBOT
#

Yes — in the output you pasted, openclaw.service is running as user openclaw, not root:

root@openclaw3:~# systemctl show openclaw.service ... -p User ...
User=openclaw

The confusion is coming from this line earlier:

sudo systemctl show openclaw-gateway.service -p User ...
User=

That’s a different unit name (openclaw-gateway.service) and it looks like it either doesn’t exist / isn’t the one that’s actually running, so User= is empty there.

If you want a “no doubt” verification, check the actual process owner:

#
pid=$(systemctl show -p MainPID --value openclaw.service)
ps -o user=,pid=,cmd= -p "$pid"

That will print the Unix user for the running gateway process. Even if you started it from a root shell, systemd will still run the service as User=openclaw when that’s set in the unit.

elder topaz
#

root@openclaw3:~# pid=$(systemctl show -p MainPID --value openclaw.service)
ps -o user=,pid=,cmd= -p "$pid"
error: process ID out of range

Usage:
ps [options]

Try 'ps --help <simple|list|output|threads|misc|all>'
or 'ps --help <s|l|o|t|m|a>'
for additional help text.

For more details see ps(1).

rustic ridgeBOT
#

That ps: process ID out of range basically means "$pid" ended up being empty or 0.

Quickly confirm what systemd thinks the PID is right now:

systemctl show openclaw.service -p User -p MainPID -p ExecMainPID -p ControlPID
systemctl status openclaw.service --no-pager | sed -n '1,12p'

Then try a more robust PID extraction:

pid="$(systemctl show openclaw.service -p MainPID | cut -d= -f2)"
echo "pid=[$pid]"
ps -o user=,pid=,cmd= -p "$pid"
#

If MainPID shows as 0 (can happen if systemd isn’t tracking it the way you expect), just locate the running gateway process directly:

pgrep -a -u openclaw -f "openclaw gateway" || true
ps -fu openclaw | grep -i openclaw | head

But even without ps, your earlier systemctl show openclaw.service ... output is already strong evidence:

  • User=openclaw → systemd intends to run it as openclaw
  • EnvironmentFiles=/opt/openclaw.env + the service is active → that’s the env context the running gateway gets

Paste the output of the systemctl show ... MainPID ... line above and we can see why it’s coming back empty/0 in your shell.

elder topaz
#

openclaw secrets audit --check
error: unknown command 'secrets'

rustic ridgeBOT
#

openclaw secrets … is a newer CLI command group. If your CLI says:

error: unknown command 'secrets'

…that means the openclaw you’re actually running (you’re on 2026.2.12) doesn’t include the secrets subcommands yet (or you have an older openclaw earlier in $PATH than you think).

Quick confirm

Run these and paste output:

command -v openclaw
openclaw --version
openclaw help | grep -E "^  secrets\\b" || true

How to get the secrets commands

Update OpenClaw to a newer version, then retry:

#
sudo openclaw update --dry-run
sudo openclaw update

(Or pick a channel: sudo openclaw update --channel stable|beta|dev.)

Docs you were following (applies once you’re on a version that has it):
https://docs.openclaw.ai/cli/secrets

elder topaz
#

OpenClaw version: 2026.2.12

which min version require to have this secrets?

rustic ridgeBOT
#

openclaw secrets … first shows up in v2026.2.26 (and v2026.2.26-beta.1).

So if you’re on 2026.2.12, you’ll get exactly what you saw: “unknown command 'secrets'”.

To use it, update to >= 2026.2.26 (ideally latest stable), then re-run:

openclaw secrets audit --check

Docs (once you’re on a new enough version): https://docs.openclaw.ai/cli/secrets

elder topaz
#

great.. so generate a warning block related to this new finding for my blog that i wrote for using this vault stuff instead of openclaw.env...

rustic ridgeBOT
#
> **Warning (version prerequisite): `openclaw secrets` / “vault” workflow requires OpenClaw ≥ v2026.2.26**
>
> If you’re on an older build (common on VPS “1‑click” images), the CLI will error with:
> `error: unknown command 'secrets'`.
>
> Before migrating away from plaintext env files like `/opt/openclaw.env`, **upgrade OpenClaw to v2026.2.26+** and confirm the command group exists:
>
> - `openclaw --version`
> - `openclaw help | grep -E '^  secrets\b'`
>
> Only then proceed with the secrets workflow (audit/configure/apply). Otherwise you’ll be stuck in a half-migrated state where docs mention `openclaw secrets …`, but your installed CLI can’t run it.
elder topaz
#

Enable bash shell completion for openclaw?

shall i say yes or no

rustic ridgeBOT
#

It’s purely a quality-of-life thing (tab-completion for openclaw … commands). It does not affect the Gateway/service, networking, or security.

  • Say “Yes” if you actually use bash interactively on that account and want tab completion.

    • It will add a small “OpenClaw Completion” block to your shell profile and source a cached script (per docs: openclaw completion --shell bash --install).
  • Say “No” if you want to keep the server/user dotfiles untouched (common on VPS), or you’re not sure which user it’s installing for (root vs openclaw). You can always enable later.

You can enable it later anytime (for the user you care about) with:

openclaw completion --shell bash --install

And removing it is just deleting that “OpenClaw Completion” block from ~/.bashrc / ~/.bash_profile.

elder topaz
#

just now updated opneclaw to verstion 2026.3.1

then added the following two snippets to openclaw.json

"secrets": {
"providers": {
"onepassword_nvidia": {
"source": "exec",
"command": "/home/openclaw/.local/bin/op",
"args": ["read", "op://Shared with Yug/NVIDIA_API_KEY/password"],
"passEnv": ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
"jsonOnly": false
}
}
},

and

replaced "apiKey": "${NVIDIA_API_KEY}",
to "apiKey": { "source": "exec", "provider": "onepassword_nvidia", "id": "value" },

and then restarted the systemd service using 'systemctl restart openclaw'

Then the dashboard came up and said Version
n/a
Health
Offline

and agent on telegram said below:

Agent failed before reply: Sandbox security: bind mount "/home/openclaw/homebrew:/home/openclaw/homebrew:ro" source "/home/openclaw/homebrew" is outside allowed roots (/home/openclaw/.openclaw/workspace). Use a dangerous override only when you fully trust this runtime.
Logs: openclaw logs --follow

and i see on the dasboard > config

Config
Edit ~/.openclaw/openclaw.json safely.
origin not allowed (open the Control UI from the gateway host or allow it in gateway.controlUi.allowedOrigins)>> and then the text gets cut off .. seems some dasboard ui issue..

rustic ridgeBOT
#

You’ve got two separate problems here, and they line up perfectly with the symptoms:

1) Dashboard shows Version n/a + Health Offline + “origin not allowed …”

That usually means: the Control UI loaded in your browser, but the browser’s WebSocket connection back to the gateway is being rejected because gateway.controlUi.allowedOrigins isn’t set (required for non-loopback binds like lan).

Since you’re binding via env: OPENCLAW_GATEWAY_BIND=lan, fix is:

  • Add in openclaw.json:
{
  gateway: {
    controlUi: {
      allowedOrigins: [
        "http://<YOUR_VPS_IP>:18789"
        // or "https://<YOUR_DOMAIN>" if behind TLS/proxy
#
      ],
    },
  },
}

Important: the origin must match exactly what’s in your browser URL bar (scheme + host + port).
Then:

sudo systemctl restart openclaw

After this, the dashboard should stop showing n/a/offline.

Docs reference: gateway.controlUi.allowedOrigins (config ref)

#

https://docs.openclaw.ai/gateway/configuration-reference (Gateway → controlUi.allowedOrigins)


2) Telegram error: Sandbox security: bind mount "/home/openclaw/homebrew:..." is outside allowed roots

This is coming from your agent sandbox Docker config, not from secrets.

Somewhere in your config you likely have:

  • agents.defaults.sandbox.docker.binds: ["/home/openclaw/homebrew:/home/openclaw/homebrew:ro"]

In newer versions (incl 2026.3.1), OpenClaw enforces that bind sources must be inside the workspace roots unless you explicitly break-glass it.

Preferred fix (safe):

Remove that bind mount (if you don’t need it).

Quickly locate it:

#
grep -n "homebrew" /home/openclaw/.openclaw/openclaw.json

Break-glass fix (only if you fully trust this VPS runtime):

{
  agents: {
    defaults: {
      sandbox: {
        docker: {
          dangerouslyAllowExternalBindSources: true,
        },
      },
    },
  },
elder topaz
#

le'ts get the dashboard online first.. i reverted the secrets config in json, yet the dahboard is not up yet...

the i added

"gateway": {
"port": 18789,
"mode": "local",
"bind": "loopback",
"auth": {
"mode": "token",
"token": "${OPENCLAW_GATEWAY_TOKEN}"
},
"trustedProxies": [
"127.0.0.1"
],
"tailscale": {
"mode": "off",
"resetOnExit": false
},
"controlUi": {
"allowedOrigins": [
"https://<ip>"
]
}
},

The ip is the same ip i use to access my dashboard from browser.. there is no port...

Also, i just noticed that the same config is at several places...
OPENCLAW_GATEWAY_PORT=18789
OPENCLAW_GATEWAY_BIND=lan
is present in the /opt/openclaw.env which is linked to the systemd service env file

rustic ridgeBOT
#

Your config changes aren’t taking effect the way you expect because systemd env vars win over openclaw.json, and your Control UI origin string probably doesn’t exactly match what the browser is using.

1) You currently have two sources of truth (and env overrides config)

Because your service loads /opt/openclaw.env, these will override whatever you put in openclaw.json:

  • OPENCLAW_GATEWAY_PORT=18789
  • OPENCLAW_GATEWAY_BIND=lan

So even if you set:

gateway: { bind: "loopback" }

…the service is still effectively on bind=lan (and port=18789) unless you remove/change those env vars.

Pick one:

#
  • If you want to control bind/port via openclaw.json: remove (or comment out) those two lines from /opt/openclaw.env.
  • If you want to control bind/port via /opt/openclaw.env: then set your openclaw.json gateway bind/port to match (or just omit them to avoid confusion).

After changing /opt/openclaw.env, restart:

sudo systemctl restart openclaw

2) allowedOrigins must match the browser origin exactly (scheme + host + port)

The value must match what the browser reports as location.origin (no path).

On the machine where you open the dashboard, open DevTools Console and run:

location.origin

Whatever it prints must be in:

#
gateway: { controlUi: { allowedOrigins: [ "..." ] } }

Examples:

  • If you open http://1.2.3.4:18789/openclaw → origin is http://1.2.3.4:18789
  • If you open http://1.2.3.4/openclaw → origin is http://1.2.3.4
  • If you open https://your-domain.com/openclaw → origin is https://your-domain.com

Right now you set https://<ip> (no port) — that’s only correct if you really are loading the UI over HTTPS on 443 (typically via a reverse proxy). Also: no trailing slash.

3) Quick check: what does the service actually think it’s running with?

Run and paste:

#
openclaw gateway status

That command is great because it usually shows Config (cli) vs Config (service), and you’ll immediately see if systemd env is overriding your config.

If you paste:

  1. the output of location.origin
  2. openclaw gateway status

…I can tell you the exact allowedOrigins line to use and whether you should keep bind=lan or loopback for your setup.

elder topaz
#

the gateway is down.. therefore location.origin is returning null on chrome > inspector > console

commented
OPENCLAW_GATEWAY_PORT=18789
OPENCLAW_GATEWAY_BIND=lan
and restarted.. but dashboard still down

now bind=loopback in the json.. idk if i should change it back to lan


recent status... it was a typo in json, which i just corrected and the dashboard is down...

root@openclaw3:~# openclaw gateway status

🦞 OpenClaw 2026.3.1 (2a8ac97)
I keep secrets like a vault... unless you print them in debug logs again.



Service: systemd (disabled)
File logs: /tmp/openclaw-0/openclaw-2026-03-02.log

Service config looks out of date or non-standard.
Service config issue: Gateway service PATH is not set; the daemon should use a minimal PATH.
Recommendation: run "openclaw doctor" (or "openclaw doctor --repair").
Config (cli): ~/.openclaw/openclaw.json (missing)
Config (service): ~/.openclaw/openclaw.json (missing)

Gateway: bind=loopback (127.0.0.1), port=18789 (env/config)
Probe target: ws://127.0.0.1:18789
Dashboard: http://127.0.0.1:18789/
Probe note: Loopback-only gateway; only local clients can connect.

Runtime: stopped (state inactive, sub dead, last exit 0, reason 0)
RPC probe: failed
RPC target: ws://127.0.0.1:18789
gateway url override requires explicit credentials
Fix: pass --token or --password (or gatewayToken in tools).
Config: /root/.openclaw/openclaw.json

Troubles: run openclaw status
Troubleshooting: https://docs.openclaw.ai/troubleshooting
root@openclaw3:~#

rustic ridgeBOT
#

Two key takeaways from that openclaw gateway status output:

  1. You ran it as root, so it’s looking for config in /root/.openclaw/openclaw.json (which is missing).
  2. Your gateway is set to bind=loopback, which means you cannot open the dashboard via the VPS IP from your laptop (only from the VPS itself, or via a tunnel / reverse proxy).

Let’s get the dashboard reachable again with the smallest, safest checks.

Step 1 — confirm the gateway is actually running (service health)

Run as root:

sudo systemctl status openclaw.service --no-pager
sudo journalctl -u openclaw.service -n 200 --no-pager
sudo ss -ltnp | grep 18789 || true

This tells us if anything is listening on 18789 and why it might be crashing.

#

Step 2 — stop mixing root vs openclaw config paths

When you run openclaw … as root you’ll keep seeing “config missing” unless you either:

Option A (recommended): run the CLI as the service user

sudo -iu openclaw openclaw gateway status
sudo -iu openclaw openclaw status

Option B: run as root but point OpenClaw at openclaw’s home

sudo OPENCLAW_HOME=/home/openclaw openclaw gateway status

(That makes the CLI read /home/openclaw/.openclaw/... while still letting you use root permissions.)

Step 3 — decide: do you want to access the dashboard via VPS IP or via tunnel?

#

If you want to open it directly in your browser via VPS IP:

Set bind back to lan (either in /opt/openclaw.env or in openclaw.json, but not both), and set allowedOrigins to the exact browser origin.

Most common direct case is:

  • URL you open: http://<VPS_IP>:18789/
  • then allowedOrigins must include: http://<VPS_IP>:18789 (http, includes port)

Example:

gateway: {
  bind: "lan",
  port: 18789,
  controlUi: {
    allowedOrigins: ["http://<VPS_IP>:18789"],
  },
}
#

If you put https://<ip> with no port, that only makes sense if you have a reverse proxy serving TLS on 443. If you don’t have that proxy, it won’t match reality.

If you want to keep it safer (loopback) and still use the UI:

Keep bind: "loopback" and use an SSH tunnel from your laptop:

ssh -L 18789:127.0.0.1:18789 root@<VPS_IP>

Then open: http://127.0.0.1:18789/ locally.


If you paste the output of:

  • systemctl status openclaw.service --no-pager (top part is enough)
  • journalctl -u openclaw.service -n 80 --no-pager
#

…I can tell you whether the gateway is actually down/crashing, or just unreachable due to loopback/origin config.

elder topaz
#

root@openclaw3:~# sudo systemctl status openclaw.service --no-pager
● openclaw.service - Openclaw Gateway Service
Loaded: loaded (/etc/systemd/system/openclaw.service; enabled; preset: enabled)
Active: active (running) since Mon 2026-03-02 23:11:49 UTC; 3s ago
Main PID: 326701 (openclaw)
Tasks: 14 (limit: 2315)
Memory: 223.5M (peak: 223.8M)
CPU: 3.474s
CGroup: /system.slice/openclaw.service
├─326701 openclaw
└─326708 openclaw

Mar 02 23:11:49 openclaw3 systemd[1]: openclaw.service: Scheduled restart job, restart counter is at 52.
Mar 02 23:11:49 openclaw3 systemd[1]: Started openclaw.service - Openclaw Gateway Service.
Mar 02 23:11:49 openclaw3 (openclaw)[326701]: openclaw.service: Referenced but unset environment …Y_PORT
Hint: Some lines were ellipsized, use -l to show in full.

Mar 02 23:12:39 openclaw3 openclaw[326768]: 2026-03-02T23:12:39.270+00:00 Invalid port
Mar 02 23:12:39 openclaw3 systemd[1]: openclaw.service: Main process exited, code=exited, status=1/FAILURE
Mar 02 23:12:39 openclaw3 systemd[1]: openclaw.service: Failed with result 'exit-code'.
Mar 02 23:12:39 openclaw3 systemd[1]: openclaw.service: Consumed 4.785s CPU time.

#

root@openclaw3:~# sudo ss -ltnp | grep 18789 || true
root@openclaw3:~#

rustic ridgeBOT
#

That systemctl status output is the smoking gun:

  • openclaw.service: Referenced but unset environment …Y_PORT
  • then shortly after: Invalid port → gateway exits → systemd restarts (restart counter 52)

This happens when the systemd unit’s ExecStart references an env var for the port (almost certainly OPENCLAW_GATEWAY_PORT) and you commented it out in /opt/openclaw.env. systemd substitutes it as empty → OpenClaw gets a blank/invalid --port value.

Fix the crash loop (do this first)

  1. Put the port back in /opt/openclaw.env as a plain integer:
OPENCLAW_GATEWAY_PORT=18789

(Do not leave it as OPENCLAW_GATEWAY_PORT= and don’t add quotes.)

  1. Restart and check logs with full lines:
#
sudo systemctl restart openclaw.service
sudo systemctl status -l openclaw.service --no-pager
sudo journalctl -u openclaw.service -n 80 -l --no-pager

If you paste the Referenced but unset environment ... line from the -l output, we can confirm the exact variable name, but I’m 99% sure it’s the gateway port var.

About bind=loopback vs lan (why your browser can’t see the dashboard)

  • bind=loopback means it only listens on 127.0.0.1you cannot open it via the VPS IP from your laptop.
  • If you want to access the dashboard directly via the VPS IP, set bind to lan (either via env or json, but pick one place).

Since your service is driven by /opt/openclaw.env, simplest is set:

OPENCLAW_GATEWAY_BIND=lan
#
OPENCLAW_GATEWAY_PORT=18789

and then remove/omit gateway.bind / gateway.port from openclaw.json to avoid conflicts/confusion.

allowedOrigins detail (once the gateway is actually running)

If you are not using a TLS reverse proxy, the browser origin will be http, not https, and it usually includes the port:

  • http://<VPS_IP>:18789

So allowedOrigins would look like:

gateway: {
  controlUi: {
#
    allowedOrigins: ["http://<VPS_IP>:18789"],
  },
}

But let’s first get the service to stop crashing (port env var).

#

ss showing nothing on 18789 means the gateway is not listening at all right now → the systemd service is still crashing / not starting (or it hit the restart limit and stopped).

Given your earlier log line (Referenced but unset environment …Y_PORT + Invalid port), the next thing to do is confirm which env var is missing and put it back.

1) Show the full error + last 100 lines (no ellipsizing)

sudo systemctl status -l openclaw.service --no-pager
sudo journalctl -u openclaw.service -n 120 -l --no-pager

2) Verify the gateway env vars are actually present (port/bind)

sudo grep -nE 'OPENCLAW_GATEWAY_(PORT|BIND|TOKEN)' /opt/openclaw.env

If OPENCLAW_GATEWAY_PORT is missing/empty/commented, add it back as a plain integer, e.g.

#
OPENCLAW_GATEWAY_PORT=18789

3) If systemd got stuck in “failed” after too many restarts

(after you fix the env file)

sudo systemctl reset-failed openclaw.service
sudo systemctl restart openclaw.service
sudo ss -ltnp | grep 18789 || true

4) If it still won’t come up, grab the file log (usually has the exact parse error)

Your openclaw gateway status earlier showed file logs under /tmp/openclaw-0/…:

#
ls -t /tmp/openclaw-0/openclaw-*.log | head -1
tail -n 200 "$(ls -t /tmp/openclaw-0/openclaw-*.log | head -1)"

Paste the journalctl output (step 1) and the grep OPENCLAW_GATEWAY_… output (step 2) and we can get it stable + listening again before touching allowedOrigins/dashboard config.

elder topaz
#

the dashboard came up as soon as i uncommented port from .env file... so i deleted port from json and left it in .env file...

same i did for bind... it is not only in .env file and =lan

and my final config in .json is
"gateway": {
"mode": "local",
"auth": {
"mode": "token",
"token": "${OPENCLAW_GATEWAY_TOKEN}"
},
"trustedProxies": [
"127.0.0.1"
],
"tailscale": {
"mode": "off",
"resetOnExit": false
},
"controlUi": {
"allowedOrigins": [
"https://<ip>"
]
}
},

tell me where i am wrong.. also, my telegram version still throws the same errror when i say 'hi'
Agent failed before reply: Sandbox security: bind mount "/home/openclaw/homebrew:/home/openclaw/homebrew:ro" source "/home/openclaw/homebrew" is outside allowed roots (/home/openclaw/.openclaw/workspace). Use a dangerous override only when you fully trust this runtime.
Logs: openclaw logs --follow

And my dashboard show Version ${APP_VERSION} at the top right corner

rustic ridgeBOT
#

You’re very close — you’ve got 2 separate “wrongness” issues now:

A) Dashboard shows Version ${APP_VERSION} → you have a literal ${…} value in the gateway process env

The Control UI reads the version from the gateway “hello” handshake (server.version). The gateway computes that from env vars:

  • OPENCLAW_VERSION
  • OPENCLAW_SERVICE_VERSION
  • npm_package_version

If the UI shows ${APP_VERSION}, then one of those is literally set to that string (most commonly people have something like this in /opt/openclaw.env):

OPENCLAW_SERVICE_VERSION=${APP_VERSION}
# or
OPENCLAW_VERSION=${APP_VERSION}
#

Important: systemd EnvironmentFile= does not expand ${APP_VERSION}. It stays literal → exactly what you’re seeing.

Fix: remove those lines entirely (recommended), or set them to a real value (e.g. OPENCLAW_SERVICE_VERSION=2026.3.1).

Check with:

sudo grep -nE 'OPENCLAW_(VERSION|SERVICE_VERSION)|APP_VERSION' /opt/openclaw.env
sudo systemctl restart openclaw

After restart, the UI should show something like 2026.3.1.


B) Telegram “hi” fails: sandbox bind mount /home/openclaw/homebrew ... outside allowed roots

This is unrelated to allowedOrigins and unrelated to secrets. It means your config still contains a sandbox bind like:

#
  • agents.defaults.sandbox.docker.binds: ["/home/openclaw/homebrew:/home/openclaw/homebrew:ro"]

Newer OpenClaw versions enforce “bind sources must be inside the allowed roots” (by default the workspace), so that mount is blocked and the agent fails before reply.

Find where it’s set:

sudo grep -n "homebrew" /home/openclaw/.openclaw/openclaw.json
# also check env file in case it was configured there
sudo grep -n "homebrew" /opt/openclaw.env

Fix options:

  • Preferred: remove that bind mount entirely (most people don’t need it).
  • Break-glass (less safe): set
    agents.defaults.sandbox.docker.dangerouslyAllowExternalBindSources: true
#

…but I’d only do that if you truly need to mount host dirs into the sandbox.


C) controlUi.allowedOrigins: your current value is probably wrong for a direct-IP dashboard

If you are opening the dashboard as:

  • http://<VPS_IP>:18789/

then allowedOrigins must include:

  • http://<VPS_IP>:18789 (note http and port)

Your current https://<ip> (no port) is only correct if you’re actually serving the UI through an HTTPS reverse proxy on 443.

If you tell me the exact URL you type in the browser (copy/paste it), I’ll tell you the exact allowedOrigins line.

#

If you paste the output of these two greps, we can finish this in one shot:

sudo grep -nE 'OPENCLAW_(VERSION|SERVICE_VERSION)|APP_VERSION' /opt/openclaw.env
sudo grep -n "homebrew" /home/openclaw/.openclaw/openclaw.json
elder topaz
#

commented OPENCLAW_VERSION=${APP_VERSION} from openclaw.env and now the dashboard says
Version dev

i am not sure what is it suppose to say, but i thought it will automtically pull the actual verssion.. i don't want to hard-code it as it may be out of date as soon as i upgrade again..

rustic ridgeBOT
#

Version dev in the dashboard just means: the gateway didn’t advertise a real version string, so the UI fell back to "dev".

Why it happened in your case:

  • The gateway reports server.version from env vars in this order: OPENCLAW_VERSIONOPENCLAW_SERVICE_VERSIONnpm_package_version → fallback "dev".
  • Your systemd setup (the DO 1‑click openclaw.service) doesn’t set OPENCLAW_SERVICE_VERSION, and npm_package_version usually isn’t present in systemd services.
  • Setting OPENCLAW_VERSION=${APP_VERSION} in /opt/openclaw.env can’t work because systemd EnvironmentFile does not expand ${…}, so you literally injected ${APP_VERSION} into the process env (which is why you saw it verbatim).

What it “should” say

Ideally it should show 2026.3.1 (or whatever you’re running).

Best fix (no hard-coding, and also fixes your “service config looks out of date” warning)

Switch to / repair the standard OpenClaw-managed service, which sets OPENCLAW_SERVICE_VERSION automatically:

sudo openclaw doctor --repair
#
# or (more forceful service rewrite)
sudo openclaw gateway install --force

Then restart the service.

This also addresses the earlier warning you saw about the systemd service PATH being non-standard/out of date.

Quick verification (optional)

Check whether the running gateway process has a version env var at all:

pid=$(systemctl show -p MainPID --value openclaw.service)
sudo tr '\0' '\n' < /proc/$pid/environ | grep -E 'OPENCLAW_(VERSION|SERVICE_VERSION)=' || true
#

If neither is present, the UI will keep showing dev until the service is repaired/reinstalled to include OPENCLAW_SERVICE_VERSION.

elder topaz
#

"sandbox": {
"mode": "all",
"workspaceAccess": "rw",
"docker": {
"network": "bridge",
"binds": [
"/home/openclaw/homebrew:/home/openclaw/homebrew:ro",
"/opt/openclaw:/opt/openclaw:ro",
"/usr/lib/node_modules/openclaw/skills:/home/openclaw/.openclaw/workspace/skills:ro"
]
}
}

this is my current json agents.defaults.sandbox

give me the correct one

rustic ridgeBOT
#

That sandbox config is “wrong” only because of the binds: in 2026.3.1 OpenClaw blocks bind-mount sources that are outside the sandbox’s allowed roots (which, for your setup, is /home/openclaw/.openclaw/workspace). So any bind like /home/openclaw/homebrew:... or /opt/openclaw:... will fail the sandbox security check and the agent will error before replying.

Recommended “correct” sandbox config (safe)

Remove the external binds completely:

sandbox: {
  mode: "all",
  workspaceAccess: "rw",
  docker: {
    network: "bridge",
    // IMPORTANT: no external bind mounts
    // (omit binds entirely, or set it to [])
    binds: [],
  },
}
#

That will stop the Telegram “hi” failure.

If you really need those external mounts (not recommended)

Then you’d have to use the break-glass flag:

sandbox: {
  mode: "all",
  workspaceAccess: "rw",
  docker: {
    network: "bridge",
    dangerouslyAllowExternalBindSources: true,
    binds: [
      "/home/openclaw/homebrew:/home/openclaw/homebrew:ro",
      "/opt/openclaw:/opt/openclaw:ro",
    ],
elder topaz
#

i ran repair as root.. and saw /root/openclaw.. don't know if got created just now.. because earlier i renamed it to openclaw.bak.. but it got created again... is it because i ran these commands as root like doctor repair?

rustic ridgeBOT
#

Yes — exactly.

When you run openclaw doctor --repair (or most openclaw … commands) as root, OpenClaw treats root as “the user”, so it uses root’s home for state/config:

  • ~ = /root
  • state/config default to /root/.openclaw/...

So it will (re)create /root/.openclaw/ even if you previously renamed it, because from root’s perspective that’s the correct default location.

What you should do instead (for your setup)

Your gateway service runs as user openclaw, so you want doctor/repair to target that user’s state:

Option A (best): run doctor as the openclaw user

sudo -iu openclaw openclaw doctor --repair
#

Option B: run as root but force OpenClaw to use openclaw’s HOME

sudo OPENCLAW_HOME=/home/openclaw openclaw doctor --repair

That way it repairs /home/openclaw/.openclaw/... (the one your service actually uses), not /root/.openclaw.

What about /root/.openclaw now?

It’s just root’s separate state dir. You can leave it alone, or delete it later if you’re sure you don’t need it — but don’t mix it with the openclaw user’s state.

elder topaz
#

openclaw@openclaw3:~$ sudo -iu openclaw openclaw doctor --repair
openclaw is not in the sudoers file.

rustic ridgeBOT
#

That error is because you’re currently logged in as the openclaw user, and on your VPS that user isn’t allowed to run sudo (totally normal).

The fix is simply: run the “switch user” command from a root shell, not from inside the openclaw shell.

Option 1 (best): run doctor as the openclaw user (but invoked by root)

From a root session:

sudo -iu openclaw openclaw doctor --repair

If you’re currently openclaw@..., first do:

exit

Option 2: use su (also from root)

#
su - openclaw -c "openclaw doctor --repair"

Option 3: run as root but target openclaw’s state (works even without sudo access for openclaw)

OPENCLAW_HOME=/home/openclaw openclaw doctor --repair

(That prevents it from touching /root/.openclaw and instead repairs /home/openclaw/.openclaw.)

If option 1 fails, paste the output of:

sudo -iu openclaw whoami
sudo -iu openclaw command -v openclaw
elder topaz
#

root@openclaw3:~# sudo -iu openclaw openclaw doctor --repair

OpenClaw 2026.3.1 (2a8ac97)
Welcome to the command line: where dreams compile and confidence segfaults.

┌ OpenClaw doctor

◇ Startup optimization ──╮
│ │
│ - NODE_COMPILE_CACHE is not set; repeated CLI runs can be slower on small hosts (Pi/VM). │
│ - OPENCLAW_NO_RESPAWN is not set to 1; set it to avoid extra startup overhead from │
│ self-respawn. │
│ - Suggested env for low-power hosts: │
│ export NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache │
│ mkdir -p /var/tmp/openclaw-compile-cache │
│ export OPENCLAW_NO_RESPAWN=1 │
│ │


◇ Config ───────────────────────────────────────────────────╮
│ │
│ Config invalid; doctor will run with best-effort config. │
│ │

◇ Doctor warnings ───────╮
│ │
│ - channels.telegram.groupPolicy is "allowlist" but groupAllowFrom (and allowFrom) is │
│ empty — all group messages will be silently dropped. Add sender IDs to │
│ channels.telegram.groupAllowFrom or channels.telegram.allowFrom, or set groupPolicy to │
│ "open". │
│ │

there is more output.. let's see what's your say here

rustic ridgeBOT
#

That snippet of doctor --repair output is already telling you two useful things:

1) “Config invalid; doctor will run with best-effort config.”

This means your /home/openclaw/.openclaw/openclaw.json parsed, but failed schema validation (wrong key name, wrong type, unknown fields, etc.). Doctor will keep going, but it’s important to scroll further down because it usually prints which config paths are invalid and what it repaired/ignored.

So: paste the next chunk of output after that point (especially anything that looks like “invalid at … path …” / “expected … got …” / “will write …”).

2) Telegram warning: groupPolicy="allowlist" but groupAllowFrom is empty

This one is not a “crash” — it’s explaining why Telegram group chats will be silently dropped.

If you don’t care about Telegram groups (you only DM the bot), you can ignore this warning.

If you do want group chats to work, pick one:

Option A (open groups)

#
channels: {
  telegram: {
    groupPolicy: "open",
  },
}

Option B (restrict groups to your Telegram user ID)

channels: {
  telegram: {
    groupPolicy: "allowlist",
    groupAllowFrom: ["123456789"], // your numeric Telegram user id
  },
}
#

(Reference: https://docs.openclaw.ai/channels/telegram — see groupPolicy + groupAllowFrom.)


Next message to paste

Please paste the rest of the doctor output after that warning block — that’s where we’ll see what’s making the config “invalid” and what doctor actually repaired.

elder topaz
#

here's the complete doctor output.. give me the shortest playbook and one line explainations if you feel so

rustic ridgeBOT
#

I didn’t actually receive the “complete doctor output” in your message (looks like it got swallowed/trimmed), but based on everything you’ve shown so far, here’s the shortest playbook that fixes the known blockers.

Short playbook (with 1‑line “why” each)

0) Always operate on the same state dir the service uses

Why: running openclaw … as root creates/uses /root/.openclaw and you end up fixing the wrong config.

Use this prefix for all commands below:

sudo -iu openclaw openclaw <command>
# OR (if you prefer root):
sudo OPENCLAW_HOME=/home/openclaw openclaw <command>

1) Fix Telegram “hi” failing (sandbox bind mounts)

#

Why: your sandbox binds include host paths outside the allowed roots, so the agent fails before replying.

Remove the binds (recommended):

sudo -iu openclaw openclaw config set agents.defaults.sandbox.docker.binds '[]' --strict-json
sudo systemctl restart openclaw.service

(Do not use dangerouslyAllowExternalBindSources unless you truly need those mounts.)


2) Fix Control UI origin issues (if you access dashboard via VPS IP)

Why: allowedOrigins must match the browser origin exactly; https://<ip> is wrong unless you actually serve HTTPS via a proxy.

If you open the dashboard as http://<VPS_IP>:18789/, set:

#
gateway: {
  controlUi: {
    allowedOrigins: ["http://<VPS_IP>:18789"],
  },
}

Then:

sudo systemctl restart openclaw.service

3) Fix dashboard showing Version dev

Why: your current systemd service doesn’t set OPENCLAW_SERVICE_VERSION, and you removed the broken ${APP_VERSION} line (good), so it falls back to "dev".

#

Best non-hardcoded fix: let OpenClaw rewrite/repair the service metadata:

sudo OPENCLAW_HOME=/home/openclaw openclaw doctor --repair
# if it still stays "dev":
sudo OPENCLAW_HOME=/home/openclaw openclaw gateway install --force
sudo systemctl restart openclaw.service

(That’s the path that makes the supervised service include the correct version env automatically.)


4) Telegram group warning (only if you care about groups)

Why: groupPolicy="allowlist" + empty groupAllowFrom means group messages get dropped silently.

Either open groups:

#
sudo -iu openclaw openclaw config set channels.telegram.groupPolicy "open"

or allowlist your numeric Telegram user id:

sudo -iu openclaw openclaw config set channels.telegram.groupAllowFrom '["123456789"]' --strict-json

Restart after:

sudo systemctl restart openclaw.service

Verify (fast)

#
sudo -iu openclaw openclaw gateway status
sudo ss -ltnp | grep 18789
sudo -iu openclaw openclaw logs --follow

If you paste the doctor section that says which config fields are invalid (usually a few lines with paths), I can give you an even tighter “change exactly these 2–3 lines” follow-up.

elder topaz
rustic ridgeBOT
#

Playbook (based on your doctor output) — minimal steps, 1‑liners.

1) Run CLI with the same env the systemd service uses

Why: your config has ${NVIDIA_API_KEY} (and likely token vars) but sudo -iu openclaw … does not load /opt/openclaw.env, so config read fails and doctor can’t auth to the gateway.

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw doctor --repair'

(Use that pattern for any openclaw CLI command that needs env-var substitution.)

2) Fix the sandbox bind-mount crash (your Telegram “hi” failure)

Why: /home/openclaw/homebrew and /opt/openclaw are outside the allowed sandbox roots, so the agent fails before replying.

Set binds to empty:

#
sudo -iu openclaw openclaw config set agents.defaults.sandbox.docker.binds '[]' --strict-json
sudo systemctl restart openclaw.service

3) (Optional) Fix Telegram group drops warning

Why: groupPolicy="allowlist" + empty groupAllowFrom means group messages are silently ignored.

Either open groups:

sudo -iu openclaw openclaw config set channels.telegram.groupPolicy "open"

or allowlist your numeric Telegram user id:

sudo -iu openclaw openclaw config set channels.telegram.groupAllowFrom '["<your_numeric_id>"]' --strict-json
#

Restart after:

sudo systemctl restart openclaw.service

4) Ignore these unless you care

  • Memory search embedding provider missing → only affects semantic recall; doesn’t break chat.
  • Port already in use → just means your gateway service is already running on 18789 (normal).

5) “Version dev” in dashboard

Why: you removed the broken OPENCLAW_VERSION=${APP_VERSION} (good) and your current systemd unit doesn’t provide a real version string, so the UI falls back to dev. Cosmetic, not functional.

If you want, paste your /opt/openclaw.env with secrets redacted and I’ll point out exactly which lines to keep/remove so CLI + service behave consistently.

#

From that doctor output (pastebin), you have 3 actual blockers. Here’s the shortest playbook.

1) Make the CLI see the same env vars as the systemd service

Why: doctor/config load is failing on ${NVIDIA_API_KEY} and the gateway connect is failing with token mismatch because your interactive openclaw run isn’t loading /opt/openclaw.env.

Run doctor like this (important: source the env file):

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw doctor --repair'

(Use that pattern for openclaw status, openclaw gateway status, etc., whenever your config references ${…}.)

2) Fix Telegram “hi” failing (sandbox bind mount security)

Why: agents.defaults.sandbox.docker.binds contains host paths outside the allowed roots, so the agent crashes before replying.

Set binds to empty:

#
sudo -iu openclaw openclaw config set agents.defaults.sandbox.docker.binds '[]' --strict-json
sudo systemctl restart openclaw.service

3) (Optional) Stop Telegram group messages being silently dropped

Why: channels.telegram.groupPolicy="allowlist" but groupAllowFrom is empty.

Either open groups:

sudo -iu openclaw openclaw config set channels.telegram.groupPolicy "open"

or allowlist your numeric Telegram user id:

sudo -iu openclaw openclaw config set channels.telegram.groupAllowFrom '["<your_id>"]' --strict-json
#

Restart after:

sudo systemctl restart openclaw.service

Ignore these doctor lines

  • “Memory search embedding provider missing” → only affects semantic recall.
  • “systemd user services are unavailable” → you’re using a system-wide systemd service; it’s noisy but not the root problem.

If after step (1) you still get “gateway token mismatch”, paste:

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw gateway status'
elder topaz
rustic ridgeBOT
#

That pastebin output is still showing the old failure modes:

  • MissingEnvVarError: Missing env var "NVIDIA_API_KEY" …
  • unauthorized: gateway token mismatch …

So your source /opt/openclaw.env step didn’t actually make those vars visible to the openclaw CLI run (most commonly because /opt/openclaw.env is not readable by the openclaw user, even though systemd can read it as root).

Shortest fix (make CLI + service consistent)

Step 1 — put the required vars into OpenClaw’s global .env for the openclaw user

Why: OpenClaw auto-loads /home/openclaw/.openclaw/.env for both the service user and interactive CLI, no sourcing needed.

  1. Create it with correct perms:
sudo install -o openclaw -g openclaw -m 600 /dev/null /home/openclaw/.openclaw/.env
#
  1. Add (copy/paste) these two lines into /home/openclaw/.openclaw/.env:
NVIDIA_API_KEY=...             # your value
OPENCLAW_GATEWAY_TOKEN=...     # must match what the gateway uses

Then restart:

sudo systemctl restart openclaw.service

✅ Verify without leaking secrets:

sudo -iu openclaw bash -lc 'openclaw doctor'

If that runs without the NVIDIA missing-env error, you’re good.

#

After that: fix the Telegram “hi” crash

Your sandbox bind mounts are still the reason Telegram fails. Set binds empty:

sudo -iu openclaw openclaw config set agents.defaults.sandbox.docker.binds '[]' --strict-json
sudo systemctl restart openclaw.service

Quick check (why your earlier command didn’t work)

Run:

sudo -iu openclaw bash -lc 'test -r /opt/openclaw.env && echo readable || echo NOT_READABLE'
#

If it prints NOT_READABLE, that explains why sourcing didn’t help.

If you want, paste the output of that one-liner and ls -l /opt/openclaw.env (permissions only; redact nothing else) and I’ll tell you whether it’s better to keep /opt/openclaw.env or migrate everything to /home/openclaw/.openclaw/.env.

elder topaz
#

no, fetch the pastebin again... https://pastebin.com/raw/ERREG0iu

i updated it with the output of root@openclaw3:~# sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw doctor --repair'

rustic ridgeBOT
#

I re-fetched that pastebin and it’s still the same output (and my fetch shows it was served from cache). Pastebin “raw” gets cached pretty aggressively — easiest is: make a new paste or change the URL to .../raw/ERREG0iu?cb=1 (any querystring) so helpers don’t hit cache.

That said, the output you pasted still contains these two hard failures:

  • MissingEnvVarError: NVIDIA_API_KEY → config env-substitution still not seeing it
  • unauthorized: gateway token mismatch → CLI isn’t using the same gateway token as the running service

Shortest playbook (works even if /opt/openclaw.env is root-only)

Run the CLI as root (so it can read /opt/openclaw.env), but force it to use openclaw’s state dir:

sudo bash -lc 'set -e; set -a; source /opt/openclaw.env; set +a; export OPENCLAW_HOME=/home/openclaw; \
  echo NVIDIA_API_KEY=${NVIDIA_API_KEY:+set}; echo OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN:+set}; \
  openclaw doctor --repair'

(Those echo …=set lines confirm the env vars are actually present without leaking values.)

#

If either prints empty, then /opt/openclaw.env isn’t setting it in a bash-sourceable way.

Then fix the Telegram “hi” crash (sandbox binds)

You already identified the cause. Set binds to empty and restart:

sudo OPENCLAW_HOME=/home/openclaw openclaw config set agents.defaults.sandbox.docker.binds '[]' --strict-json
sudo systemctl restart openclaw.service

If token mismatch persists after that

Confirm the running gateway process has a token (masked):

pid=$(systemctl show -p MainPID --value openclaw.service)
sudo tr '\0' '\n' < /proc/$pid/environ | grep OPENCLAW_GATEWAY_TOKEN | sed 's/=.*$/=<set>/'
#

If the service has it but the CLI doesn’t, you still have an env visibility problem (and the root+OPENCLAW_HOME wrapper above is the cleanest workaround).

If you want, paste (1) the output of that wrapper command and (2) the first ~20 lines of /opt/openclaw.env with secrets redacted, and I’ll point out the exact formatting/var that’s breaking substitution.

elder topaz
rustic ridgeBOT
#

Yep — this new paste looks healthy. Key changes vs the old one:

  • No more MissingEnvVarError: NVIDIA_API_KEY → sourcing /opt/openclaw.env worked.
  • No more “gateway token mismatch” noise.
  • Telegram probe is ok: Telegram: ok (@openclaw3_bookkeeper_bot)
  • Doctor completes cleanly.

So at this point you only have one real warning left in that output:

Remaining warning: Telegram group allowlist is empty

Meaning: if you message the bot in a Telegram group, it will drop messages.

Fix options (pick one):

A) You don’t use groups → ignore it

Nothing to do.

#

B) Allow all Telegram groups (more open)

sudo -iu openclaw openclaw config set channels.telegram.groupPolicy "open"
sudo systemctl restart openclaw.service

C) Keep allowlist, add your user id (recommended if you do use groups)

sudo -iu openclaw openclaw config set channels.telegram.groupAllowFrom '["<your_numeric_telegram_user_id>"]' --strict-json
sudo systemctl restart openclaw.service

(You can get your numeric id by asking your bot to print it, or using a “get my id” bot—up to you.)

Next thing (not shown in doctor output): your earlier Telegram “hi” crash

That one was caused by sandbox bind mounts (/home/openclaw/homebrew…). Doctor doesn’t show it here, so either you already fixed it, or it will still happen when the agent actually runs.

#

Quick confirm by sending “hi” again. If it still fails, remove binds:

sudo -iu openclaw openclaw config set agents.defaults.sandbox.docker.binds '[]' --strict-json
sudo systemctl restart openclaw.service

If this thread is solved now, please right-click the solution message → Apps → ✅ Mark Solution so it gets indexed on Answer Overflow.

elder topaz
#

telegram "hi" crash is solved by removing the binds...

however, what is this telegram allowlist stuff.. explain me

rustic ridgeBOT
#

Telegram “allowlist stuff” is just access control: who is allowed to trigger your bot in DMs vs in groups.

OpenClaw treats DMs and groups separately because group chats are much riskier (anyone can poke the bot, cost money, prompt-inject it, etc.), so the default is usually fail-closed.

The two knobs that matter

1) channels.telegram.dmPolicy

Controls what happens in DMs.

Common values:

  • pairing (common default): you have to pair/approve first
  • allowlist: only users in channels.telegram.allowFrom can DM
  • open: anyone can DM (usually not recommended)
  • disabled: ignore DMs

2) channels.telegram.groupPolicy

Controls what happens in groups.

#

Values:

  • allowlist (default in many setups): only allow specific senders
  • open: allow anyone in groups
  • disabled: ignore groups entirely

What the warning means

Your config has:

  • channels.telegram.groupPolicy = "allowlist"
  • but both channels.telegram.groupAllowFrom and channels.telegram.allowFrom are empty

So OpenClaw is warning: all Telegram group messages will be silently dropped, because there’s nobody on the allowlist.

What you should do (pick based on your use case)

A) You only use Telegram DMs (no groups):
You can ignore the warning, or explicitly disable groups to silence it:

#
channels: { telegram: { groupPolicy: "disabled" } }

B) You want the bot to work in groups, but only for you:
Keep allowlist, add your numeric Telegram user id:

channels: {
  telegram: {
    groupPolicy: "allowlist",
    groupAllowFrom: ["123456789"] // your Telegram numeric user id
  }
}

C) You want the bot to respond to anyone in any group (not recommended):

#
channels: { telegram: { groupPolicy: "open" } }

If you tell me whether you want groups enabled or disabled, I’ll suggest the cleanest config for your setup.

elder topaz
#
  1. So before i either disable the telegram groupPolicy or allowlist myself, can you tell me the usecases where people use it in groups.. that way i will decide accordingly
  2. what is Skills status box?
  3. what is Startup optimization?
  4. which plugis it is talkina bout? what is plugin? idk if i downloaded any
rustic ridgeBOT
#

0) Telegram groupPolicy / allowlist — why it exists + when people use groups

Telegram groups are the “public surface area” of your bot. Without a policy, anyone in the group can trigger it (cost, spam, prompt-injection risk, accidental secrets).

Common group use cases where people intentionally enable it:

  • Family/household group: “remind us”, shopping list, travel planning, shared calendar-ish nudges.
  • Team/project group: meeting summaries, decision logging, release checklists, “what did we decide last week?” (shared context is the point).
  • Ops/alerts group: bot posts status + people ask follow-ups (“what’s failing?”) in the same thread.
  • Community “ask the bot” room: only if you really want public access (usually with strict guardrails).

So the decision is basically:

  • If you only DM the bot → set channels.telegram.groupPolicy: "disabled" and ignore the warning forever.
  • If you want it in groups but only you can trigger it → keep allowlist and set groupAllowFrom: ["<your_numeric_telegram_user_id>"].
  • If you want anyone in groups to trigger it → groupPolicy: "open" (rarely recommended).

Docs pointer: https://docs.openclaw.ai/channels/telegram (see groupPolicy + groupAllowFrom)

#

1) What is the Skills status box in openclaw doctor?

It’s a quick inventory of optional “skills” (integrations/features) and whether your machine can run them.

  • Eligible: skills that could run in your setup (OS/tools available).
  • Missing requirements: skills that are installed/known, but you’re missing dependencies (e.g. ffmpeg, python, docker, etc.). Not an error unless you need that skill.
  • Blocked by allowlist: skill exists but your config/tool policy doesn’t allow it.

You can mostly ignore this unless you’re trying to use a specific skill and it’s not working.


2) What is Startup optimization?

That section is about speeding up repeated CLI runs on small machines/VMs:

  • NODE_COMPILE_CACHE=... → caches Node’s compiled output so the CLI starts faster.
#
  • OPENCLAW_NO_RESPAWN=1 → avoids extra “self-respawn” overhead the CLI sometimes does.

It’s performance advice, not required for correctness.


3) What are Plugins? (and why does it say you have some)

Plugins are Gateway extensions loaded in-process (extra channels/tools/features). OpenClaw ships with a bunch of bundled plugins that are disabled by default, so seeing something like “Loaded: 5, Disabled: 33” does not mean you installed 38 things manually.

If you want to see exactly what they are:

openclaw plugins list
openclaw plugins doctor

CLI docs: https://docs.openclaw.ai/cli/plugins

elder topaz
#
  1. so my i know what all skills are eligble, and what all skilss under missing requirements are and what they missing? and where this inventory of 45+6 came from? it this something come by default on installing openclaw, like bundled?
  2. are plugins different than skill? i am confused
rustic ridgeBOT
#

1) How do you see which skills are eligible / missing reqs (and what’s missing)?

Use the skills CLI (it’s exactly what Doctor is summarizing):

# summary (counts + lists)
openclaw skills check

# list everything; add -v to show missing details column
openclaw skills list -v

# only the ready ones
openclaw skills list --eligible

# deep dive on one skill (shows exactly which bins/env/config gates are failing + install hints)
openclaw skills info <skill-name>
#

If your config uses ${…} env-var substitution (like your NVIDIA key), run it in the same env context as the service:

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw skills check'

Where do the “45 + 6” numbers come from?
Doctor scans the skill inventory and evaluates each skill’s metadata.openclaw.requires gates (bins/env/config/os). The counts are just:

  • Eligible = requirements satisfied (and not disabled/blocked)
  • Missing requirements = requirements not satisfied
  • Blocked = blocked by skills.allowBundled allowlist (if you set it)

Is it bundled by default?
Yes: OpenClaw ships with a bunch of bundled skills (plus any you add yourself). Skills are loaded from 3 places (highest precedence first):

  1. <workspace>/skills
  2. ~/.openclaw/skills
#
  1. bundled skills shipped with OpenClaw

Docs: https://docs.openclaw.ai/tools/skills and https://docs.openclaw.ai/cli/skills


2) Plugins vs Skills — what’s the difference?

They’re different layers:

  • Skill = mostly instructions (SKILL.md) that teach the agent how to use tools (and optionally declare requirements like “needs ffmpeg” or “needs OPENAI_API_KEY”).
  • Plugin = code extension loaded into the Gateway process (can add new tools, channels, commands, and can also ship skills).

So:

  • You can have skills without plugins.
  • You can have plugins that include skills.

To see what plugins you actually have:

elder topaz
#

i tried replacing "apiKey": "${NVIDIA_API_KEY}", with
"apiKey": { "source": "exec", "provider": "onepassword_nvidia", "id": "value" },

but portal threw some invalid config error

rustic ridgeBOT
#

That apiKey replacement is the right shape (model provider apiKey accepts a SecretRef object), so if the Control UI says “invalid config”, it’s almost always one of these:

The 3 common causes

1) You changed apiKey to an exec SecretRef, but didn’t define the exec provider (or misspelled it)

You need a matching entry under secrets.providers with the same provider name:

secrets: {
  providers: {
    onepassword_nvidia: {
      source: "exec",
      command: "/ABSOLUTE/PATH/TO/op",
      args: ["read", "op://Shared with Yug/NVIDIA_API_KEY/password"],
      passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
      jsonOnly: false,
#
    },
  },
},
models: {
  providers: {
    "custom-integrate-api-nvidia-com": {
      apiKey: { source: "exec", provider: "onepassword_nvidia", id: "value" },
      // baseUrl + models must still be present here as before
    },
  },
},

2) Your op path is a symlink (blocked by default)

OpenClaw’s exec-secret provider validates the executable path and rejects symlinks unless you opt in.

#

Fix by either:

  • pointing command at the real binary (best), or
  • allowing symlinks + restricting with trustedDirs:
secrets: {
  providers: {
    onepassword_nvidia: {
      source: "exec",
      command: "/home/openclaw/.local/bin/op",
      allowSymlinkCommand: true,
      trustedDirs: ["/home/openclaw/.local", "/usr/local", "/opt/homebrew"],
      args: ["read", "op://Shared with Yug/NVIDIA_API_KEY/password"],
      passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
      jsonOnly: false,
    },
#
  },
},

3) There’s still a leftover "${NVIDIA_API_KEY}" somewhere else in the config

Even if you fix this one apiKey, any remaining ${NVIDIA_API_KEY} reference will still make config load fail.

Quick check:

grep -n "NVIDIA_API_KEY" /home/openclaw/.openclaw/openclaw.json

Fastest way to know which one it is

After applying the change, restart and read the service log error (it will say exactly what’s invalid):

#
sudo systemctl restart openclaw.service
sudo journalctl -u openclaw.service -n 120 --no-pager

If you paste the exact “invalid config” / log error lines, I can tell you which of the 3 it is and the minimal edit to fix it.

Docs reference: https://docs.openclaw.ai/gateway/secrets (exec providers, symlink rules, trustedDirs).

elder topaz
#

the control ui rejected the replacement

#

the control ui rejected the replacement, but i pasted it directly in the json, and restart the openclaw and the agent said hi back to my hi...

but how do i ensure that it is now actually fetching my model key from op and not from env?

rustic ridgeBOT
#

That apiKey object shape is valid in OpenClaw 2026.3.1 (model-provider apiKey accepts a “SecretRef”, not just a string), so if the Control UI rejects it, it’s almost certainly rejecting something else in the edit (syntax) or the exec provider config.

What I need (fastest)

In the Control UI → Config editor, when it says “invalid config”, it usually shows a red error line with a path. Paste that exact line (it won’t include your secret).

The most common reasons (with fixes)

1) secrets.providers.onepassword_nvidia.command is invalid

Rules:

  • must be an absolute path
  • must be the binary only (no spaces, no quotes, no op read ... inline)

Good:

secrets: {
  providers: {
#
    onepassword_nvidia: {
      source: "exec",
      command: "/home/openclaw/.local/bin/op",
      args: ["read", "op://Shared with Yug/NVIDIA_API_KEY/password"],
      passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
      jsonOnly: false,
    },
  },
},

Bad (will be rejected):

command: "/home/openclaw/.local/bin/op read op://.../password" // ❌ spaces -> unsafe
#

2) op is a symlink (blocked unless you allow it)

If /home/openclaw/.local/bin/op is a symlink, you may need:

allowSymlinkCommand: true,
trustedDirs: ["/home/openclaw/.local", "/usr/local", "/opt/homebrew"],

You can check:

ls -l /home/openclaw/.local/bin/op

3) You only replaced apiKey, but didn’t actually define the provider

Your apiKey ref:

#
apiKey: { source: "exec", provider: "onepassword_nvidia", id: "value" }

requires that secrets.providers.onepassword_nvidia exists.

Quick “truth source” (no guessing)

Run this right after the UI rejects the config:

sudo journalctl -u openclaw.service -n 120 --no-pager

and paste the error block about “Invalid config … path …”. That will tell us exactly which field the portal didn’t like.

elder topaz
#

What I need (fastest)
In the Control UI → Config editor, when it says “invalid config”, it usually shows a red error line with a path. Paste that exact line (it won’t include your secret).

that's the whole point.. some weird ui "Settings .. other" down hides the above "config" ui.. cutting the error line...

anyways...
the control ui rejected the replacement, but i pasted it directly in the json, and restart the openclaw and the agent said hi back to my hi...

but how do i ensure that it is now actually fetching my model key from op and not from env?

rustic ridgeBOT
#

If your openclaw.json now has:

apiKey: { source: "exec", provider: "onepassword_nvidia", id: "value" }

…then OpenClaw is no longer reading ${NVIDIA_API_KEY} for that provider at all. ${…} env-substitution only happens when the config value is a string like "${NVIDIA_API_KEY}".

That said, here are the best ways to prove it’s actually pulling from op:

1) Confirm the config is really the exec ref (not still a string somewhere)

Because your provider id has hyphens, use bracket syntax:

sudo -iu openclaw openclaw config get 'models.providers["custom-integrate-api-nvidia-com"].apiKey'
#

You should see the { source:"exec", provider:"onepassword_nvidia", id:"value" } object.

Also check you didn’t leave any ${NVIDIA_API_KEY} elsewhere:

grep -n '\${NVIDIA_API_KEY}' /home/openclaw/.openclaw/openclaw.json || true

2) Force a secrets resolution (this actually runs your op exec provider)

This is the “real” verification:

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw secrets audit --check'

If op can’t be executed / token missing / path unsafe / bad output, this will fail with a clear error. If it passes, OpenClaw successfully fetched the secret via your exec provider.

3) Hard proof (optional): make the env key unusable and see that nothing breaks

#

Temporarily set the env var to junk (or comment it out) in /opt/openclaw.env, restart, and send “hi”. If it still works, the NVIDIA key is definitely not coming from env anymore.

(Only do this if you’re comfortable editing the env file; make a backup first.)


Why “models status” isn’t a great check here

openclaw models status intentionally doesn’t resolve SecretRefs (it would have to execute secret providers), so it won’t reliably tell you “this came from op”.

If you paste your secrets.providers.onepassword_nvidia block (with tokens redacted), I can sanity-check it for the common gotchas (symlinked op path, missing passEnv, etc.).

#

To be 100% sure it’s using 1Password op (exec SecretRef) and not the env var, do one of these proof checks:

A) Check what’s in the config right now

This confirms the provider apiKey is an object ref, not a "${…}" string.

sudo -iu openclaw openclaw config get 'models.providers["custom-integrate-api-nvidia-com"].apiKey'

Expected output should look like:

{ source: "exec", provider: "onepassword_nvidia", id: "value" }

Also confirm there are no leftovers:

#
grep -n '\${NVIDIA_API_KEY}' /home/openclaw/.openclaw/openclaw.json || true

B) Force OpenClaw to actually execute the secret provider (best verification)

This will run the exec provider and fail loudly if it can’t resolve.

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw secrets audit --check'
  • If it succeeds → OpenClaw can fetch secrets via your op resolver.
  • If it fails → it will tell you why (bad command path, missing OP_SERVICE_ACCOUNT_TOKEN, invalid output, etc.).

C) Definitive test: make the env key wrong and see that it still works

Temporarily set a bogus env value (or comment it out) in /opt/openclaw.env, restart, then send “hi”.

#

If it still replies and models still work, it’s not using env anymore.

(Do this only if you’re comfortable editing the env file; keep a backup.)


About the Control UI “invalid config” banner being cut off

Yeah, that’s a UI annoyance. When that happens, the most reliable way to see the real validation error is:

sudo journalctl -u openclaw.service -n 120 --no-pager

It prints the exact config path + reason even when the UI hides it.

If you paste your secrets.providers.onepassword_nvidia block (redact tokens), I can sanity-check it too.

elder topaz
#

root@openclaw3:~# sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw secrets audit --check'

🦞 OpenClaw 2026.3.1 (2a8ac97) — End-to-end encrypted, drama-to-drama excluded.

Secrets audit: findings. plaintext=4, unresolved=0, shadowed=0, legacy=0.

  • [PLAINTEXT_FOUND] /home/openclaw/.openclaw/openclaw.json:skills.entries.local-places.apiKey Skill apiKey is stored as plaintext.
  • [PLAINTEXT_FOUND] /home/openclaw/.openclaw/openclaw.json:skills.entries.nano-banana-pro.apiKey Skill apiKey is stored as plaintext.
  • [PLAINTEXT_FOUND] /home/openclaw/.openclaw/openclaw.json:skills.entries.goplaces.apiKey Skill apiKey is stored as plaintext.
  • [PLAINTEXT_FOUND] /home/openclaw/.openclaw/openclaw.json:skills.entries.notion.apiKey Skill apiKey is stored as plaintext.
    root@openclaw3:~#
rustic ridgeBOT
#

Nice — this output is exactly what you wanted:

  • unresolved=0 means all SecretRefs resolved successfully, including your models.providers.custom-integrate-api-nvidia-com.apiKey exec ref.
    If it were still relying on ${NVIDIA_API_KEY} or your op resolver was broken, you’d typically see unresolved>0 (REF_UNRESOLVED) or the audit would error out.

So you can be confident the model key is being fetched via your op exec provider.

What the audit is complaining about is separate:

What this means

plaintext=4 = you have 4 skill API keys stored as plain strings in /home/openclaw/.openclaw/openclaw.json:

  • skills.entries.local-places.apiKey
  • skills.entries.nano-banana-pro.apiKey
  • skills.entries.goplaces.apiKey
  • skills.entries.notion.apiKey
#

That’s not “broken”, it’s just a security hygiene warning.

What to do (optional)

If you want to migrate those into secret refs too, run the interactive helper:

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw secrets configure'

(or keep them plaintext if you don’t care — it’ll still run fine.)

If your main goal was “prove NVIDIA key comes from op not env”: you’re good.

elder topaz
#

I commented NVIDIA_API_KEY in the .evn file, restarted openclaw.. and the agent still working.. so i believe it is fetching from 1password.. however, to double check, i renamed the key from NOTION_API_KEY to NOTION_API_KEYY in 1password, and ran
root@openclaw3:~# sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw secrets audit --check'

🦞 OpenClaw 2026.3.1 (2a8ac97) — If you can describe it, I can probably automate it—or at least make it funnier.

Secrets audit: findings. plaintext=4, unresolved=0, shadowed=0, legacy=0.

  • [PLAINTEXT_FOUND] /home/openclaw/.openclaw/openclaw.json:skills.entries.local-places.apiKey Skill apiKey is stored as plaintext.
  • [PLAINTEXT_FOUND] /home/openclaw/.openclaw/openclaw.json:skills.entries.nano-banana-pro.apiKey Skill apiKey is stored as plaintext.
  • [PLAINTEXT_FOUND] /home/openclaw/.openclaw/openclaw.json:skills.entries.goplaces.apiKey Skill apiKey is stored as plaintext.
  • [PLAINTEXT_FOUND] /home/openclaw/.openclaw/openclaw.json:skills.entries.notion.apiKey Skill apiKey is stored as plaintext.
    root@openclaw3:~#

I thougt, this time it will show unresolved = 1 because now the NVIDIA_API_KEY is commented out in the .evn file and rename to a different name NVIDIA_API_KEYY in 1password... but there is still unresolved=0... so i am confuse

also,
skills.entries.local-places.apiKey
skills.entries.nano-banana-pro.apiKey
skills.entries.goplaces.apiKey
skills.entries.notion.apiKey
are VARIABLES
"skills": {
"entries": {
"local-places": {
"apiKey": "${GEMINI_API_KEY}"
},
"nano-banana-pro": {
"apiKey": "${GEMINI_API_KEY}"
},
"goplaces": {
"apiKey": "${GEMINI_API_KEY}"
},
"notion": {
"apiKey": "${NOTION_API_KEY}"
}
}
},

then why it it saying plain text... these all keys have been configured earlier in the .env file and are still present uncommented

rustic ridgeBOT
#

secrets audit is only testing SecretRefs (the { source, provider, id } objects) + a few legacy stores. It does not treat "${ENV_VAR}" strings as SecretRefs.

So two different mechanisms are getting mixed up in your head right now:

1) Why unresolved=0 even after you commented NVIDIA_API_KEY and renamed stuff in 1Password

A) Commenting NVIDIA_API_KEY in /opt/openclaw.env

That won’t matter if your model apiKey is now a SecretRef exec (1Password). In that case OpenClaw does not need NVIDIA_API_KEY at all.

B) Renaming NOTION_API_KEY in 1Password

That won’t affect anything unless your Notion skill is also using a SecretRef exec provider.

Right now your Notion skill is configured like:

skills.entries.notion.apiKey: "${NOTION_API_KEY}"
#

That is env substitution, not 1Password. So renaming something in 1Password won’t change it.

Also: your secrets audit output shows unresolved=0, which means: “all SecretRefs that exist in config resolved”. If your Notion key is not a SecretRef, it can’t become “unresolved” in secrets-audit.


2) Why it says PLAINTEXT_FOUND for "${GEMINI_API_KEY}" / "${NOTION_API_KEY}"

Because those fields are still strings in openclaw.json.

The audit rule is basically: if a secret-bearing field contains a string value in config, it’s considered plaintext storage, even if the string happens to be "${SOME_ENV}".

To make it not plaintext, store it as a SecretRef object, e.g.:

skills: {
  entries: {
#
    notion: {
      apiKey: { source: "env", provider: "default", id: "NOTION_API_KEY" },
    },
    goplaces: {
      apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" },
    },
  },
}

(That’s the “vault style” config. It stays structured, and the secret value never lives as a string in the JSON.)


3) How to prove NVIDIA is coming from op (and not env)

Do a “negative test” that only breaks the op path, not env:

#
  1. First confirm the model apiKey is actually the exec ref:
sudo -iu openclaw openclaw config get 'models.providers["custom-integrate-api-nvidia-com"].apiKey'
  1. Then run secrets audit with the 1Password token removed for that one command:
sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; unset OP_SERVICE_ACCOUNT_TOKEN; openclaw secrets audit --check'
  • If it fails with an exec-provider error → you have hard proof it was using op.
  • If it still passes → either the NVIDIA provider isn’t a SecretRef exec anymore, or your op command doesn’t actually depend on OP_SERVICE_ACCOUNT_TOKEN (less likely with service accounts).

#

If you want the clean “all secrets are SecretRefs” setup, run the interactive migrator:

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw secrets configure'

That will convert those skills.entries.*.apiKey strings into refs and eliminate the PLAINTEXT_FOUND findings.

elder topaz
#
  1. if "${}" is also considered plaintext, then why it didn't complain for telegram bot token and gateway token.. those two are also configured as env variable "${}" in json

rustic ridgeBOT
#
  1. openclaw secrets audit isn’t a “scan every possible secret field” tool.

It currently only inspects a few secret-capable places (you can see this in what it reports):

  • models.providers.*.apiKey
  • skills.entries.*.apiKey
  • Google Chat serviceAccount
  • auth-profiles store (auth-profiles.json)
  • plus it scans ~/.openclaw/.env (not /opt/openclaw.env)

It does not audit (today) things like:

  • channels.telegram.botToken
  • gateway.auth.token

So even if those are "${...}" strings in openclaw.json, they won’t show up in the PLAINTEXT_FOUND list because that audit doesn’t look there.

#

If you want to confirm what it actually scanned, run:

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw secrets audit --json | head -200'

and look at filesScanned + findings.


Why it does complain about your skill keys that look like "${GEMINI_API_KEY}"

Because config loading does ${ENV} substitution up front. By the time secrets audit examines the resolved config, that field is a real string api key, not the literal "${GEMINI_API_KEY}", and the audit only treats SecretRef objects as “not plaintext”.

To make those findings go away, convert those skill apiKeys from strings to SecretRefs, e.g.:

skills: {
  entries: {
#
    notion: { apiKey: { source: "env", provider: "default", id: "NOTION_API_KEY" } },
    goplaces: { apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" } },
  }
}

(or use openclaw secrets configure to do it interactively).


You wrote “2.” but didn’t include the question—what’s the second part?

elder topaz
#

so, i ran this interactive secret configure command.. shall i need to created a different provider for each api key? can you tell me a sample json showing how the relevant blocks looks like if i were to replace goplaces, notion, and nvidia key?

rustic ridgeBOT
#

You don’t need a different provider per API key.

Think of it like this:

  • A provider = “how do we fetch secrets?” (env / file / exec).
  • A SecretRef ({source, provider, id}) = “which secret do we want?”

So you usually create one exec provider for 1Password (e.g. onepassword) and then reference different ids for each key.

Important detail for source:"exec"

Exec providers in OpenClaw expect a resolver protocol (they send JSON on stdin with a list of ids, and the command must return JSON with values).
So you cannot use plain op read ... directly as a generic multi-secret provider unless you wrap it in a tiny resolver script (or you create separate providers each with fixed args and rely on jsonOnly:false single-value mode — that works, but it’s “one provider per secret”).

So you have two viable patterns:


#

Pattern A (recommended): one 1Password provider + multiple ids (requires a small wrapper)

Config shape:

{
  secrets: {
    providers: {
      onepassword: {
        source: "exec",
        command: "/usr/local/bin/openclaw-1password-resolver", // your wrapper
        passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
        jsonOnly: true
      }
    }
  },
#
  models: {
    providers: {
      "custom-integrate-api-nvidia-com": {
        // ...
        apiKey: { source: "exec", provider: "onepassword", id: "nvidia_api_key" }
      }
    }
  },

  skills: {
    entries: {
      goplaces: {
        apiKey: { source: "exec", provider: "onepassword", id: "gemini_api_key" }
      },
      notion: {
#
        apiKey: { source: "exec", provider: "onepassword", id: "notion_api_key" }
      }
    }
  }
}

(Your wrapper translates ids like gemini_api_key → an op://... path and returns the protocol JSON response.)


Pattern B (works today without writing a wrapper): one provider per secret (single-value mode)

This is the simplest with the raw op CLI, because each provider can have fixed args and return just the value.

Example:

#
{
  secrets: {
    providers: {
      onepassword_nvidia: {
        source: "exec",
        command: "/home/openclaw/.local/bin/op",
        args: ["read", "op://Shared with Yug/NVIDIA_API_KEY/password"],
        passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
        jsonOnly: false
      },
      onepassword_gemini: {
        source: "exec",
        command: "/home/openclaw/.local/bin/op",
        args: ["read", "op://Shared with Yug/GEMINI_API_KEY/password"],
        passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
#
        jsonOnly: false
      },
      onepassword_notion: {
        source: "exec",
        command: "/home/openclaw/.local/bin/op",
        args: ["read", "op://Shared with Yug/NOTION_API_KEY/password"],
        passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
        jsonOnly: false
      }
    }
  },

  models: {
    providers: {
      "custom-integrate-api-nvidia-com": {
#
        // ...
        apiKey: { source: "exec", provider: "onepassword_nvidia", id: "value" }
      }
    }
  },

  skills: {
    entries: {
      goplaces: {
        apiKey: { source: "exec", provider: "onepassword_gemini", id: "value" }
      },
      notion: {
        apiKey: { source: "exec", provider: "onepassword_notion", id: "value" }
      }
    }
#
  }
}

This is “more providers”, but no wrapper script needed.


If you tell me which approach you want

  • No custom scripting → Pattern B (provider-per-secret) is the right answer.
  • Clean single provider (best long-term) → Pattern A, but we’ll need a tiny resolver wrapper on disk.

Also: after changing these, re-run:

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw secrets audit --check'
elder topaz
#

pattern A but you help me set it up end-to-end

rustic ridgeBOT
#

Pattern A (one 1Password provider, many keys) is totally doable — but it requires a tiny “resolver” wrapper, because OpenClaw’s source:"exec" secret providers do not run op read ... directly. They run an exec resolver protocol:

  • OpenClaw sends JSON on stdin: { protocolVersion: 1, provider: "onepassword", ids: ["..."] }
  • Your command must return JSON on stdout: { protocolVersion: 1, values: { "<id>": "<secret>" } }

Below is an end-to-end setup that works on Ubuntu/systemd.


Step 0 — prerequisites

Make sure the gateway process has these env vars (in /opt/openclaw.env, since your systemd service uses that):

  • OP_SERVICE_ACCOUNT_TOKEN=... (your 1Password service account token)

And confirm op works non-interactively as the openclaw user:

#
sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; /home/openclaw/.local/bin/op --version'

(Adjust the op path if yours differs.)


Step 1 — create a mapping file (id → op:// path)

Create:

/home/openclaw/.openclaw/op-map.json

Example:

{
#
  "nvidia/apiKey": "op://Shared with Yug/NVIDIA_API_KEY/password",
  "notion/apiKey": "op://Shared with Yug/NOTION_API_KEY/password",
  "gemini/apiKey": "op://Shared with Yug/GEMINI_API_KEY/password"
}

Permissions:

sudo install -o openclaw -g openclaw -m 700 -d /home/openclaw/.openclaw/bin
sudo install -o openclaw -g openclaw -m 600 /dev/null /home/openclaw/.openclaw/op-map.json
sudo nano /home/openclaw/.openclaw/op-map.json

Step 2 — install the resolver script (the “exec provider”)

#

Create:

/home/openclaw/.openclaw/bin/op-resolver.js

#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { execFile } from "node:child_process";

const OP = "/home/openclaw/.local/bin/op"; // <-- set this to your real op path
const MAP_PATH = "/home/openclaw/.openclaw/op-map.json";

function readStdin() {
  return new Promise((resolve, reject) => {
    let data = "";
    process.stdin.setEncoding("utf8");
#
    process.stdin.on("data", (c) => (data += c));
    process.stdin.on("end", () => resolve(data));
    process.stdin.on("error", reject);
  });
}

function opRead(opPath) {
  return new Promise((resolve) => {
    execFile(OP, ["read", opPath], { timeout: 20000 }, (err, stdout, stderr) => {
      if (err) {
        resolve({ ok: false, message: (stderr || err.message || "op failed").trim() });
        return;
      }
      resolve({ ok: true, value: String(stdout ?? "").trim() });
    });
#
  });
}

const map = JSON.parse(readFileSync(MAP_PATH, "utf8"));

const raw = await readStdin();
const req = JSON.parse(raw);
const ids = Array.isArray(req.ids) ? req.ids : [];

const values = {};
const errors = {};

for (const id of ids) {
  const opPath = map[id];
  if (!opPath) {
#
    errors[id] = { message: `unknown id (missing in ${MAP_PATH})` };
    continue;
  }
  const res = await opRead(opPath);
  if (!res.ok) {
    errors[id] = { message: res.message };
    continue;
  }
  values[id] = res.value;
}

process.stdout.write(
  JSON.stringify({ protocolVersion: 1, values, ...(Object.keys(errors).length ? { errors } : {}) })
);
#

Permissions (important: must be owned by openclaw and not group/world-writable):

sudo chown openclaw:openclaw /home/openclaw/.openclaw/bin/op-resolver.js
sudo chmod 700 /home/openclaw/.openclaw/bin/op-resolver.js

Quick local test:

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; echo "{\"protocolVersion\":1,\"provider\":\"onepassword\",\"ids\":[\"nvidia/apiKey\"]}" | /home/openclaw/.openclaw/bin/op-resolver.js'

You should see JSON with "values": { "nvidia/apiKey": "..." }.


Step 3 — update openclaw.json (one provider, many refs)

Add the provider:

#
secrets: {
  providers: {
    onepassword: {
      source: "exec",
      command: "/home/openclaw/.openclaw/bin/op-resolver.js",
      passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
      jsonOnly: true
    }
  }
},

Then set your keys like:

NVIDIA model provider

#
models: {
  providers: {
    "custom-integrate-api-nvidia-com": {
      apiKey: { source: "exec", provider: "onepassword", id: "nvidia/apiKey" }
    }
  }
}

Skills (goplaces + notion)

skills: {
  entries: {
    goplaces: {
      apiKey: { source: "exec", provider: "onepassword", id: "gemini/apiKey" }
#
    },
    notion: {
      apiKey: { source: "exec", provider: "onepassword", id: "notion/apiKey" }
    }
  }
}

Restart:

sudo systemctl restart openclaw.service

Step 4 — verify it’s really using 1Password

#

This will execute the resolver and would fail if 1Password resolution is broken:

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw secrets audit --check'

If you want the “hard proof” test: temporarily unset OP_SERVICE_ACCOUNT_TOKEN and re-run the audit — it should error with an exec-provider failure.


If you paste your current models.providers.custom-integrate-api-nvidia-com block (redact baseUrl/models list if you want), I’ll tell you exactly where to splice in the apiKey SecretRef so it matches your existing config layout.

#

If you want my “operator” opinion: use one-provider-per-secret for now. It’s boring, but it’s robust and easy to debug on a VPS.

Why I’d skip the resolver (Pattern A) right now

  • It adds a custom script you have to maintain, permission correctly, and remember during upgrades/backups.
  • When it breaks, it breaks in a “why is my key missing?” way that’s harder to reason about than “this provider runs op read …”.
  • For 3–10 secrets, the “provider-per-secret” approach is totally fine.

Should this be “fixed at OpenClaw level”?

Maybe, but it’s not a “bug” today—it’s a product choice:

  • OpenClaw’s secrets.providers.*.source="exec" is intentionally a generic protocol so it can talk to Vault/sops/1Password/etc. in a consistent, batch-friendly way.
  • The raw op CLI is not a generic secret resolver: it’s a “read one thing” CLI unless you wrap it.

So there are two realistic OpenClaw-level improvements that would make Pattern A easy:

  1. Built-in 1Password exec resolver plugin
    • You configure a mapping of ids → op://… references (or item IDs), and OpenClaw does the protocol + op read calls internally.
#
  • Best UX, safest, least DIY.
  1. A simpler exec mode for “single secret command output”
    • e.g. allow SecretRefs like:
      { source:"exec", provider:"onepassword", id:"op://vault/item/field" }
      
      and OpenClaw would run command + argsTemplate(id) without requiring the JSON protocol.
    • Easier, but it weakens the “single clean contract” a bit.

If you think this would help others (I think it would—1Password is common), it’s worth filing a GitHub issue as a feature request with your concrete example + desired config UX:
https://github.com/openclaw/openclaw/issues

Practical next step (recommended)

Stick with Pattern B, but standardize it so it doesn’t get messy:

  • one provider per secret:
#
  • onepassword_nvidia
    • onepassword_gemini
    • onepassword_notion
  • all use the same command + passEnv and just differ in args

That keeps it readable and still removes secrets from .env/config strings.

If you want, paste your current secrets.providers... block (redact tokens) and I’ll suggest a clean naming/layout.

rustic ridgeBOT
#

Fetched your paste. Here’s what I’d change / what is (and isn’t) possible with those providers.

1) Verify the secret providers block

All of these are syntactically fine as “one provider per secret” (jsonOnly:false + id:"value"). Two important notes:

A) Copy/paste bug in onepassword_openclaw_gateway_token

You have it reading the Telegram token path:

"args": ["read", "op://Shared with Yug/TELEGRAM_BOT_TOKEN/password"]

That should obviously point to your gateway token item, e.g.:

"args": ["read", "op://Shared with Yug/OPENCLAW_GATEWAY_TOKEN/password"]
#

B) Make sure op isn’t a symlink (or allow it)

Exec providers reject symlinked command paths unless you set:

allowSymlinkCommand: true,
trustedDirs: ["/home/openclaw/.local"]

Check with:

ls -l /home/openclaw/.local/bin/op

(If it’s a normal file, you’re fine.)


#

2) Telegram botToken + Gateway auth.token: you can’t wire these to SecretRefs (yet)

This is the key point:

  • channels.telegram.botToken is a string-only config field.
  • gateway.auth.token is a string-only config field.

So you cannot do:

botToken: { source:"exec", provider:"...", id:"value" }   // not supported
token:    { source:"exec", provider:"...", id:"value" }   // not supported

That’s why you currently still have to use either:

  • env substitution ("${TELEGRAM_BOT_TOKEN}", "${OPENCLAW_GATEWAY_TOKEN}") backed by /opt/openclaw.env, or
  • for Telegram specifically: channels.telegram.tokenFile (if you want to store it in a file) — but you’d still need some external mechanism (systemd ExecStartPre, cron, etc.) to write that file using op read.

So with your current providers, the “correct” config is:

#
channels: {
  telegram: {
    enabled: true,
    dmPolicy: "pairing",
    botToken: "${TELEGRAM_BOT_TOKEN}",   // still env/file based
    groupPolicy: "disabled",
    streaming: "partial",
  },
},

gateway: {
  mode: "local",
  auth: {
    mode: "token",
    token: "${OPENCLAW_GATEWAY_TOKEN}",  // still env/file based
#
  },
  controlUi: {
    allowedOrigins: ["https://167.172.198.220"], // only correct if you truly serve https
  },
},

(And yes: secrets audit won’t flag those because it doesn’t audit those fields today.)


3) Verify the skills SecretRefs

These look correct:

#
skills: {
  entries: {
    goplaces: { apiKey: { source:"exec", provider:"onepassword_gemini_api_key", id:"value" } },
    notion:  { apiKey: { source:"exec", provider:"onepassword_notion_api_key", id:"value" } },
  }
}

That should eliminate the PLAINTEXT_FOUND warnings for those skills once you remove any leftover "${GEMINI_API_KEY}" / "${NOTION_API_KEY}" strings.


Quick sanity check

After editing + restarting:

#
sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw secrets audit --check'

If you want, paste your ls -l /home/openclaw/.local/bin/op output and I’ll tell you whether you need the allowSymlinkCommand/trustedDirs bits.

#

Perfect — that’s not a symlink, it’s:

  • a regular file
  • owned by openclaw
  • executable

So you do not need allowSymlinkCommand or trustedDirs for that provider. Your command: "/home/openclaw/.local/bin/op" is in the “happy path”.

Given that, your per-secret providers should be solid. The only real fix from your paste is still the copy/paste mistake:

  • onepassword_openclaw_gateway_token currently points at the Telegram token item path — change it to the gateway token’s 1Password item.

If everything is working now, please right-click the solution message → Apps → ✅ Mark Solution so Answer Overflow indexes it.

elder topaz
rustic ridgeBOT
#

That pastebin is just doctor complaining about semantic memory search (the memory_search tool), not your Telegram/models setup.

What it means

OpenClaw’s memory search needs an embeddings provider (OpenAI/Gemini/Voyage/Mistral or local). Doctor can’t find any usable embeddings creds, so it says recall won’t work.

In your output it specifically can’t resolve keys for:

  • openai
  • google (Gemini embeddings)
  • voyage
  • mistral

2 choices

Option A — you don’t care about semantic memory search → disable it

sudo -iu openclaw openclaw config set agents.defaults.memorySearch.enabled false
#
sudo systemctl restart openclaw.service

Option B — you want memory search → give it an embeddings API key

Fastest is environment variables (what doctor is asking for):

  • OpenAI embeddings: OPENAI_API_KEY
  • Gemini embeddings: GEMINI_API_KEY
  • Voyage embeddings: VOYAGE_API_KEY
  • Mistral embeddings: MISTRAL_API_KEY

Then re-check:

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw memory status --deep'
#

Note about 1Password SecretRefs here

Right now agents.defaults.memorySearch.remote.apiKey is string-only (no SecretRef object), so if you want embeddings without env vars you’d need an OpenClaw change/feature. For now, env vars (or disabling memory search) are the practical options.

Tell me which route you prefer (disable vs enable w/ Gemini/OpenAI), and I’ll give you the minimal config/env lines for it.

rustic ridgeBOT
#

That tracks — the Control UI is basically a single-page app that keeps a WebSocket open to the gateway, and Brave (especially Shields) is way more likely to wedge/block that than Firefox.

If Firefox works with the exact same URL, your gateway/config is fine — it’s a browser-side issue.

Things to try in Brave if you still want it to work there:

  1. Disable Brave Shields for the gateway URL (lion icon → Shields off), then hard reload.
  2. Open a Private window (no extensions) and test.
  3. Clear site data for that IP/domain:
    • Settings → Privacy → Site and Shields Settings → View permissions and data stored across sites → search the IP/domain → delete.
  4. In DevTools → Console / Network, look for WebSocket errors like 1006, blocked, ERR_CONNECTION, etc. If you paste that one line, it usually points to the exact knob (cookies, blocked scripts, etc.).
  5. If you have “Block scripts” or aggressive cookie blocking enabled globally, whitelist this site.

Sticking to Firefox is totally fine; it’s the most predictable browser for the dashboard.

#

Private tab working is a dead giveaway it’s profile state in Brave (not the gateway):

  • an extension
  • cached ServiceWorker/site data
  • Brave Shields/cookie settings
  • something in local storage

Fastest way to make normal tabs work:

  1. In Brave, open the dashboard URL in a normal tab → click the padlock iconSite settings → clear data / reset permissions (or “Clear site data”).
  2. Disable Shields for that site (lion icon) and reload.
  3. Check brave://extensions and temporarily disable anything privacy/network related (adblock, script blockers, etc.) and retry.

If you want the minimal “find the culprit” approach: disable all extensions, confirm it works, then re-enable one by one.

rustic ridgeBOT
#

If it works in a private window but not your normal Brave profile, it’s almost always one of these:

  1. An extension (private windows usually have extensions disabled unless you explicitly allow them)
  2. Persisted site storage (localStorage / Service Worker / cache) that didn’t actually get cleared
  3. A stale saved token/connection setting in the normal profile (private starts fresh)

Here’s the quickest “make normal Brave work” checklist:

1) Disable extensions (fastest confirmation)

In Brave normal window:

  • go to brave://extensions
  • toggle all extensions off
  • reload the dashboard URL

If it suddenly works, re-enable extensions one-by-one to find the culprit (adblock/script/privacy ones are typical).

2) Clear all site storage via DevTools (stronger than “cookies”)

#

On the broken tab:

  • open DevTools (F12)
  • Application tab → Storage
  • click Clear site data (check everything: local storage, session storage, cache storage, service workers)

Then hard reload: Ctrl+Shift+R (or Cmd+Shift+R on macOS).

3) Turn Shields off for that site

Lion icon → Shields OFF → reload.

4) Use a fresh URL (avoid bookmarks)

If you’re using a bookmarked dashboard URL, regenerate it (so you don’t keep reusing stale auth/token state). Use whatever command you’re using to print the dashboard URL again (often openclaw dashboard), then paste that fresh URL into Brave.


If it still won’t load, tell me what “not loading” means:

#
  • blank page?
  • page loads but shows Health: Offline?
  • any message like “origin not allowed” or “unauthorized”?

And if you can, paste the first red error line from Brave DevTools → Console (no need to paste the full URL if it contains a token).

#

That message is the gateway’s auth rate limiter kicking in.

What happened: after you cleared site data, Brave no longer had the token in localStorage, so the Control UI started auto-retrying the WebSocket connection with no/incorrect token. After maxAttempts, the gateway locks you out for lockoutMs and you see:

unauthorized: too many failed attempts (retry later)

Fix (fast)

  1. Close the dashboard tab (to stop the auto-retry spam).
  2. Wait for the lockout window to expire (default is commonly ~5 minutes).
  3. Re-open and make sure you provide the correct token immediately.

To get the correct URL/token from the gateway host:

sudo -iu openclaw openclaw dashboard --no-open

(or grab it directly via OPENCLAW_GATEWAY_TOKEN from /opt/openclaw.env / your config).

#

Fix (immediate, if you don’t want to wait)

Restarting the gateway usually clears the in-memory lockout:

sudo systemctl restart openclaw.service

Then open the dashboard again and paste the right token right away.

Optional: see the actual limiter settings

sudo -iu openclaw openclaw config get gateway.auth.rateLimit

If you paste that rateLimit block (no secrets there), I can tell you exactly what your lockout duration is.

elder topaz
#

kindly open your thread separately... you jumped into mine and mixed up the context...

#

ignore @summer brook messages previously and remove his context...

rustic ridgeBOT
#

Yep — agreed.

For anything browser-specific / “unauthorized: too many failed attempts” / Brave caching, please open a separate #help thread so this one stays focused on your original NVIDIA_API_KEY / env + secrets migration context.

If you still need help in this thread, tell me where you’re at right now (current OpenClaw version + whether openclaw --version works as openclaw when you source /opt/openclaw.env, and whether secrets audit --check is clean).

#

Got it — I’ll ignore anything that isn’t from you and keep this thread focused on your VPS/OpenClaw env + secrets + Control UI situation.

I can’t “remove” other messages from the Discord thread, but I won’t use them as context.

What’s the next thing you want to tackle right now?

  • fix openclaw --version failing as user openclaw (env mismatch),
  • finish migrating remaining skills.entries.*.apiKey to SecretRefs,
  • or the dashboard version showing dev / ${APP_VERSION}?
#

Let’s keep this thread focused on hubpa’s NVIDIA_API_KEY / env / secrets setup.

F4NT0M — if you still need help with the Brave/Control UI behavior, please open a separate #help thread (happy to help there), and include the first Console error line + whether private window works.

flint lotus
#

This thread is getting very long and answers may not be accurate due to the large context. Please start a new thread for any different problems/topics. @rustic ridge please sum up the answer to the initial message and the conversation briefly.

rustic ridgeBOT
#

Summary of the original issue + what we found/fixed in this thread (keep future topics in new threads):

Root cause (initial “MissingEnvVarError: NVIDIA_API_KEY”)

  • The gateway runs as a systemd service (openclaw.service) with EnvironmentFiles=/opt/openclaw.env.
  • Interactive shells (even as user openclaw) do not automatically load /opt/openclaw.env, so config values like "...": "${NVIDIA_API_KEY}" fail during config load.

Verified service/config locations

  • Service: /etc/systemd/system/openclaw.serviceUser=openclaw, EnvironmentFiles=/opt/openclaw.env, ExecStart=/usr/bin/openclaw gateway ...
  • Config: /home/openclaw/.openclaw/openclaw.json
  • Env: /opt/openclaw.env

Practical fix / reliable CLI pattern

Run CLI commands in the same env context as the service:

sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw doctor --repair'

(This avoids both missing provider keys and gateway token mismatches.)