#10258: fix(config): preserve ${ENV_VAR} references when writing config (#9813)
stale
## Summary
- When the gateway rewrites `openclaw.json`, `${ENV_VAR}` template references were expanded to their actual secret values, leaking credentials into a file that could be committed to git
- Add `collectConfigEnvRefs()` to capture `${VAR}` template locations from the on-disk config before expansion
- Add `restoreConfigEnvVarRefs()` to restore the original template strings when writing back
- Integrate into `writeConfigFile()` so env var references are preserved through read-modify-write cycles
Fixes #9813
## Test plan
- [x] 17 new unit tests for `collectConfigEnvRefs` and `restoreConfigEnvVarRefs`
- [x] Tests cover: flat/nested/array structures, inline vars, changed values, full roundtrip
- [x] All 43 tests pass (26 original + 17 new)
- [x] `pnpm check` passes (0 warnings, 0 errors)
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
- Adds env-var reference tracking/restoration (`collectConfigEnvRefs`, `restoreConfigEnvVarRefs`) so `${ENV_VAR}` templates survive read/modify/write cycles instead of being persisted as secrets.
- Integrates the preservation step into `writeConfigFile()` by re-reading the on-disk config (with `$include` resolution) and restoring matching templates before JSON serialization.
- Extends unit tests to cover collection/restoration across flat/nested/array structures and inline `${VAR}` usage.
<h3>Confidence Score: 2/5</h3>
- This PR should not be merged until the env-ref preservation logic is made reliable under repeated calls and failure scenarios.
- Core idea is sound and tests cover basic roundtrips, but the current implementation has a stateful global-regex bug that can miss `${VAR}` refs, and the write path silently falls back to writing expanded secrets when restoration fails (including when env vars are missing). These issues directly undermine the security goal (#9813).
- src/config/env-substitution.ts, src/config/io.ts
<!-- greptile_other_comments_section -->
**Context used:**
- Context from `dashboard` - CLAUDE.md ([source](https://app.greptile.com/review/custom-context?memory=fd949e91-5c3a-4ab5-90a1-cbe184fd6ce8))
- Context from `dashboard` - AGENTS.md ([source](https://app.greptile.com/review/custom-context?memory=0d0c8278-ef8e-4d6c-ab21-f5527e322f13))
<!-- /greptile_comment -->
Most Similar PRs
#21164: feat(config): add lenient mode to resolveConfigEnvVars — preserve g...
by Mellowambience · 2026-02-19
82.6%
#21173: fix(config): support envFiles option in resolveConfigEnvVars for ga...
by Mellowambience · 2026-02-19
82.1%
#8139: fix(config): block dangerous environment variables from config.env
by yubrew · 2026-02-03
75.5%
#10729: fix: replace existsSync+readFileSync with try-catch in env-file
by Yida-Dev · 2026-02-06
75.0%
#9200: Fix: Strip dangerous env vars from baseEnv in host execution
by vishaltandale00 · 2026-02-05
74.7%
#5823: fix(config): exit cleanly on invalid config instead of high CPU loop
by gavinbmoore · 2026-02-01
74.1%
#13958: Claude/secureclaw env supabase dz q hh
by kbpranay · 2026-02-11
73.9%
#23055: test: clear gateway env vars in beforeEach to prevent leakage
by thinstripe · 2026-02-21
73.8%
#21668: fix(config): block dangerous environment variable keys from config ...
by AI-Reviewer-QS · 2026-02-20
73.6%
#23339: fix: use snapshot.parsed for env ref restoration during migrate
by Mathew-Harvey · 2026-02-22
73.5%