fix: preserve ${VAR} env var references when writing config back to disk (#11560)

* fix: preserve ${VAR} env var references when writing config back to disk

Fixes #11466

When config is loaded, ${VAR} references are resolved to their plaintext
values. Previously, writeConfigFile would serialize the resolved values,
silently replacing "${ANTHROPIC_API_KEY}" with "sk-ant-api03-..." in the
config file.

Now writeConfigFile reads the current file pre-substitution, and for each
value that matches what a ${VAR} reference would resolve to, restores the
original reference. Values the caller intentionally changed are kept as-is.

This fixes all 50+ writeConfigFile call sites (doctor, configure wizard,
gateway config.set/apply/patch, plugins, hooks, etc.) without requiring
any caller changes.

New files:
- src/config/env-preserve.ts — restoreEnvVarRefs() utility
- src/config/env-preserve.test.ts — 11 unit tests

* fix: remove global config env snapshot race

* docs(changelog): note config env snapshot race fix

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
AkosCz
2026-02-13 18:53:17 -06:00
committed by GitHub
parent 11ab1c6937
commit a4f4b0636f
7 changed files with 611 additions and 11 deletions

View File

@@ -6,6 +6,7 @@ import {
loadConfig,
parseConfigJson5,
readConfigFileSnapshot,
readConfigFileSnapshotForWrite,
resolveConfigSnapshotHash,
validateConfigObjectWithPlugins,
writeConfigFile,
@@ -170,7 +171,7 @@ export const configHandlers: GatewayRequestHandlers = {
);
return;
}
const snapshot = await readConfigFileSnapshot();
const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
if (!requireConfigBaseHash(params, snapshot, respond)) {
return;
}
@@ -209,7 +210,7 @@ export const configHandlers: GatewayRequestHandlers = {
);
return;
}
await writeConfigFile(validated.config);
await writeConfigFile(validated.config, writeOptions);
respond(
true,
{
@@ -232,7 +233,7 @@ export const configHandlers: GatewayRequestHandlers = {
);
return;
}
const snapshot = await readConfigFileSnapshot();
const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
if (!requireConfigBaseHash(params, snapshot, respond)) {
return;
}
@@ -300,7 +301,7 @@ export const configHandlers: GatewayRequestHandlers = {
);
return;
}
await writeConfigFile(validated.config);
await writeConfigFile(validated.config, writeOptions);
const sessionKey =
typeof (params as { sessionKey?: unknown }).sessionKey === "string"
@@ -371,7 +372,7 @@ export const configHandlers: GatewayRequestHandlers = {
);
return;
}
const snapshot = await readConfigFileSnapshot();
const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
if (!requireConfigBaseHash(params, snapshot, respond)) {
return;
}
@@ -413,7 +414,7 @@ export const configHandlers: GatewayRequestHandlers = {
);
return;
}
await writeConfigFile(validated.config);
await writeConfigFile(validated.config, writeOptions);
const sessionKey =
typeof (params as { sessionKey?: unknown }).sessionKey === "string"