#Control UI says invalid config + MissingEnvVarError for NVIDIA_API_KEY (DO 1‑click, 2026.2.12)
1 messages · Page 1 of 1 (latest)
That behavior is basically “service env vs interactive shell env”.
- Your systemd service (
openclaw.service) runs withEnvironmentFiles=/opt/openclaw.env, so the gateway process getsNVIDIA_API_KEYfrom systemd and can start fine. - When you do
sudo -iu openclawand runopenclaw --version, your login shell does not automatically load/opt/openclaw.env, so${NVIDIA_API_KEY}substitution in/home/openclaw/.openclaw/openclaw.jsonfails →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/.envand 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.
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
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.
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,"
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 on127.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.
/opt/openclaw.env > OPENCLAW_GATEWAY_BIND=lan
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
And if you’re connecting from your laptop over the public IP, “lan” might not be what you want; you might instead want:
tailnet(if you use Tailscale), or- explicitly open the public interface + firewall (more risky), or
- keep it
localhostand tunnel (SSH).
Docs reference: https://docs.openclaw.ai/help/environment (env var sources) and gateway troubleshooting: https://docs.openclaw.ai/gateway/troubleshooting
i think, let's leave other details for now.. i will add them later if any specific issue need it
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.serviceruns as useropenclaw - 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.
you sure - systemd service openclaw.service runs as user openclaw and not as 'root`
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.
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).
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 asopenclawEnvironmentFiles=/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.
openclaw secrets audit --check
error: unknown command 'secrets'
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
OpenClaw version: 2026.2.12
which min version require to have this secrets?
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
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...
> **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.
Enable bash shell completion for openclaw?
shall i say yes or no
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).
- It will add a small “OpenClaw Completion” block to your shell profile and source a cached script (per docs:
-
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.
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..
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,
},
},
},
},
}
Docs: https://docs.openclaw.ai/gateway/sandboxing (custom bind mounts)
Security key list: https://docs.openclaw.ai/gateway/security
If you paste (with secrets redacted) just:
- your
gateway: { ... }block - and your
agents.defaults.sandbox: { ... }block
…I can tell you exactly what to change with minimal edits.
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
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=18789OPENCLAW_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 youropenclaw.jsongateway 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 ishttp://1.2.3.4:18789 - If you open
http://1.2.3.4/openclaw→ origin ishttp://1.2.3.4 - If you open
https://your-domain.com/openclaw→ origin ishttps://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:
- the output of
location.origin 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.
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:~#
Two key takeaways from that openclaw gateway status output:
- You ran it as root, so it’s looking for config in
/root/.openclaw/openclaw.json(which is missing). - 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.
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:~#
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)
- Put the port back in
/opt/openclaw.envas a plain integer:
OPENCLAW_GATEWAY_PORT=18789
(Do not leave it as OPENCLAW_GATEWAY_PORT= and don’t add quotes.)
- 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=loopbackmeans it only listens on127.0.0.1→ you 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.
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
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_VERSIONOPENCLAW_SERVICE_VERSIONnpm_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
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..
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.versionfrom env vars in this order:OPENCLAW_VERSION→OPENCLAW_SERVICE_VERSION→npm_package_version→ fallback"dev". - Your systemd setup (the DO 1‑click
openclaw.service) doesn’t setOPENCLAW_SERVICE_VERSION, andnpm_package_versionusually isn’t present in systemd services. - Setting
OPENCLAW_VERSION=${APP_VERSION}in/opt/openclaw.envcan’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.
"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
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",
],
},
}
…but be aware this weakens the sandbox boundary a lot, so only do it if you fully trust the VPS runtime.
Docs: https://docs.openclaw.ai/gateway/sandboxing (bind mounts + security notes)
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?
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.
openclaw@openclaw3:~$ sudo -iu openclaw openclaw doctor --repair
openclaw is not in the sudoers file.
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
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
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.
here's the complete doctor output.. give me the shortest playbook and one line explainations if you feel so
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.
here's the complete doctor output.. give me the shortest playbook and one line explainations if you feel so
https://pastebin.com/raw/ERREG0iu
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'
root@openclaw3:~# sudo -iu openclaw bash -lc 'set -a; source /opt/openclaw.env; set +a; openclaw doctor --repair'
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.
- Create it with correct perms:
sudo install -o openclaw -g openclaw -m 600 /dev/null /home/openclaw/.openclaw/.env
- 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.
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'
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 itunauthorized: 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.
here's the new pastebin: https://pastebin.com/raw/brEgL6tA
Yep — this new paste looks healthy. Key changes vs the old one:
- No more
MissingEnvVarError: NVIDIA_API_KEY→ sourcing/opt/openclaw.envworked. - 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.
telegram "hi" crash is solved by removing the binds...
however, what is this telegram allowlist stuff.. explain me
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 firstallowlist: only users inchannels.telegram.allowFromcan DMopen: 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 sendersopen: allow anyone in groupsdisabled: ignore groups entirely
What the warning means
Your config has:
channels.telegram.groupPolicy = "allowlist"- but both
channels.telegram.groupAllowFromandchannels.telegram.allowFromare 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.
- 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
- what is Skills status box?
- what is Startup optimization?
- which plugis it is talkina bout? what is plugin? idk if i downloaded any
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
allowlistand setgroupAllowFrom: ["<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
Plugin system overview: https://docs.openclaw.ai/tools/plugin
- 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?
- are plugins different than skill? i am confused
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.allowBundledallowlist (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):
<workspace>/skills~/.openclaw/skills
- 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:
openclaw plugins list
Docs: https://docs.openclaw.ai/cli/plugins and https://docs.openclaw.ai/tools/plugin
i tried replacing "apiKey": "${NVIDIA_API_KEY}", with
"apiKey": { "source": "exec", "provider": "onepassword_nvidia", "id": "value" },
but portal threw some invalid config error
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
commandat 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).
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?
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.
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?
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
opresolver. - 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.
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:~#
Nice — this output is exactly what you wanted:
unresolved=0means all SecretRefs resolved successfully, including yourmodels.providers.custom-integrate-api-nvidia-com.apiKeyexec ref.
If it were still relying on${NVIDIA_API_KEY}or youropresolver was broken, you’d typically seeunresolved>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.apiKeyskills.entries.nano-banana-pro.apiKeyskills.entries.goplaces.apiKeyskills.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.
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
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:
- First confirm the model apiKey is actually the exec ref:
sudo -iu openclaw openclaw config get 'models.providers["custom-integrate-api-nvidia-com"].apiKey'
- 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
opcommand doesn’t actually depend onOP_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.
-
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
openclaw secrets auditisn’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.*.apiKeyskills.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.botTokengateway.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?
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?
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'
pattern A but you help me set it up end-to-end
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
opCLI 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:
- Built-in 1Password exec resolver plugin
- You configure a mapping of ids →
op://…references (or item IDs), and OpenClaw does the protocol +op readcalls internally.
- You configure a mapping of ids →
- Best UX, safest, least DIY.
- A simpler
execmode for “single secret command output”- e.g. allow SecretRefs like:
and OpenClaw would run{ source:"exec", provider:"onepassword", id:"op://vault/item/field" }command + argsTemplate(id)without requiring the JSON protocol. - Easier, but it weakens the “single clean contract” a bit.
- e.g. allow SecretRefs like:
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_nvidiaonepassword_geminionepassword_notion
- all use the same
command+passEnvand just differ inargs
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.
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.botTokenis a string-only config field.gateway.auth.tokenis 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 usingop 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_tokencurrently 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.
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:
openaigoogle(Gemini embeddings)voyagemistral
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.
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:
- Disable Brave Shields for the gateway URL (lion icon → Shields off), then hard reload.
- Open a Private window (no extensions) and test.
- 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.
- 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.). - 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:
- In Brave, open the dashboard URL in a normal tab → click the padlock icon → Site settings → clear data / reset permissions (or “Clear site data”).
- Disable Shields for that site (lion icon) and reload.
- Check
brave://extensionsand 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.
If it works in a private window but not your normal Brave profile, it’s almost always one of these:
- An extension (private windows usually have extensions disabled unless you explicitly allow them)
- Persisted site storage (localStorage / Service Worker / cache) that didn’t actually get cleared
- 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)
- Close the dashboard tab (to stop the auto-retry spam).
- Wait for the lockout window to expire (default is commonly ~5 minutes).
- 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.
kindly open your thread separately... you jumped into mine and mixed up the context...
ignore @summer brook messages previously and remove his context...
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 --versionfailing as useropenclaw(env mismatch), - finish migrating remaining
skills.entries.*.apiKeyto 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.
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.
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) withEnvironmentFiles=/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.service→User=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.)