#21164: feat(config): add lenient mode to resolveConfigEnvVars — preserve gateway-only env var refs without throwing
size: S
## Summary
Fixes #21130.
When a user places secrets in a systemd `EnvironmentFile=` drop-in for the gateway service and references them in `openclaw.json` via `${VAR}` syntax, **all CLI commands** (`openclaw doctor`, `openclaw gateway status`, etc.) fail with `MissingEnvVarError` — because the CLI process does not inherit the gateway's systemd environment.
## Root cause
`resolveConfigEnvVars` (via `substituteString` in `env-substitution.ts`) unconditionally throws `MissingEnvVarError` for every unresolved `${VAR}` reference. There is no way to tell it "this var is gateway-only — leave it alone for read purposes."
## Fix
Add an optional `{ lenient?: boolean }` parameter to `resolveConfigEnvVars`.
When `lenient: true`:
- Unresolvable `${VAR}` tokens are preserved as literal `${VAR}` strings instead of throwing.
- Vars that **are** present in the environment are still substituted normally.
- The default strict behaviour is completely unchanged — no existing call sites are affected.
## Changes
### `src/config/env-substitution.ts`
- Add exported `ResolveEnvVarsOptions` type (`{ lenient?: boolean }`)
- `substituteString` accepts options; preserves `${VAR}` literal when `lenient` and var is missing
- `substituteAny` threads options through recursive calls
- `resolveConfigEnvVars` signature updated: third arg `options: ResolveEnvVarsOptions = {}`
### `src/config/config.env-vars.test.ts`
New `describe("lenient mode")` block — 4 tests:
1. `${VAR}` preserved as literal when var missing and `lenient: true`
2. Present vars still resolved normally in lenient mode
3. Default strict mode still throws `MissingEnvVarError` (regression guard)
4. Nested arrays/objects with multiple gateway-only refs all preserved
## Migration note
No breaking changes. The `options` parameter is optional with a default of `{}`. Existing callers are unaffected.
## Diff summary
```diff
export function resolveConfigEnvVars(
obj: unknown,
env: NodeJS.ProcessEnv = process.env,
+ options: ResolveEnvVarsOptions = {},
): unknown {
- return substituteAny(obj, env, "");
+ return substituteAny(obj, env, "", options);
}
```
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Added lenient mode to `resolveConfigEnvVars` that preserves unresolved `${VAR}` references as literals instead of throwing, solving CLI failures when config references gateway-only environment variables.
**Key changes:**
- New optional `lenient` parameter in `resolveConfigEnvVars` (backward compatible)
- When enabled, missing env vars preserved as `${VAR}` strings rather than throwing `MissingEnvVarError`
- Present vars still substituted normally in lenient mode
- Comprehensive test coverage for lenient behavior
**Issues found:**
- Syntax error in test file: extra closing brace on line 63 breaks the test suite structure
<h3>Confidence Score: 4/5</h3>
- Safe to merge after fixing the syntax error in the test file
- The implementation is solid and well-designed with proper backward compatibility, comprehensive tests, and clear documentation. The lenient mode logic correctly preserves unresolved vars while still substituting present ones. However, there's a critical syntax error (extra closing brace) that will prevent the test suite from running properly, which must be fixed before merge.
- `src/config/config.env-vars.test.ts` has a syntax error that must be fixed
<sub>Last reviewed commit: f1507a4</sub>
<!-- greptile_other_comments_section -->
<sub>(5/5) You can turn off certain types of comments like style [here](https://app.greptile.com/review/github)!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#21173: fix(config): support envFiles option in resolveConfigEnvVars for ga...
by Mellowambience · 2026-02-19
88.9%
#10258: fix(config): preserve ${ENV_VAR} references when writing config (#9...
by nu-gui · 2026-02-06
82.6%
#8139: fix(config): block dangerous environment variables from config.env
by yubrew · 2026-02-03
78.4%
#5823: fix(config): exit cleanly on invalid config instead of high CPU loop
by gavinbmoore · 2026-02-01
78.1%
#11455: fix(gateway): default gateway.mode to local when unset
by AnonO6 · 2026-02-07
77.7%
#9200: Fix: Strip dangerous env vars from baseEnv in host execution
by vishaltandale00 · 2026-02-05
77.6%
#19885: test(gateway,browser): isolate tests from ambient OPENCLAW_GATEWAY_...
by NewdlDewdl · 2026-02-18
76.8%
#15983: feat(exec): support env field in tools.exec config
by Imccccc · 2026-02-14
76.0%
#23055: test: clear gateway env vars in beforeEach to prevent leakage
by thinstripe · 2026-02-21
75.9%
#21459: fix(gateway): resolve port from profile config, not inherited env
by kkeeling · 2026-02-19
75.6%