Setting any local config value writes the credentials inherited from the global config into the Val's .vt/config.yaml, even when the command has nothing to do with auth.
Reproduction
With a global config already containing an apiKey, i.e. after vt login:
cd my-val
vt config set --local dangerousOperations.confirmation false
cat .vt/config.yaml
apiKey: vtwn_REDACTED
refreshToken: REDACTED
dangerousOperations:
confirmation: false
The apiKey and refreshToken were never typed; they came from the global config. That command is the documented example from the README's config section.
Cause
configSetCmd bases its write on a merged read (src/cmd/lib/config.ts:138):
const config = await vtConfig.loadConfig(); // deep-merges global + local
const updatedConfig = setNestedProperty(config, key, value);
await vtConfig.saveLocalConfig(updatedConfig); // writes the whole merge locally
loadConfig() is correct as a read — callers want global-with-local-on-top. The bug is using that resolved view as the basis for a scoped write. saveLocalConfig validates and stringifies whatever it's handed, so the inherited values land in the file as if they'd been set locally.
The existing mitigation doesn't fire. offerToAddToGitignore() is gated on key === "apiKey" && !useGlobal (config.ts:159), so it only prompts when someone deliberately sets a local key. The leak happens on any local set, silently. This is the concern raised in #223 ("I'm a bit concerned about people mistakenly commiting their local configs with their apiKey to git... Any ideas here?") — the mechanism turns out not to require the user to have typed the key at all.
Symmetric case: the same merged object flows into saveGlobalConfig on the useGlobal branch, so a global vt config set run inside a Val with local overrides promotes those overrides into the global config permanently. (Read off the code path; I didn't test this one to avoid clobbering my real global config.)
Possible fix — a scoped write should read only the scope it writes. VTConfig has no single-scope loader today (only merged loadConfig, path getters, and the two savers), so it'd need something like loadLocalConfig()/loadGlobalConfig() that parses one file without merging; configSetCmd then picks its base by scope. loadConfig() stays as-is for readers. Happy to open a PR if that direction sounds right — didn't want to presume, given #223 intentionally simplified this area.
Verified against upstream/main at 1eb6413, so the line numbers should match what a maintainer sees. Ping me with any changes, and drop the issue number here once it's up if you want me to pick up the fix.
Setting any local config value writes the credentials inherited from the global config into the Val's
.vt/config.yaml, even when the command has nothing to do with auth.Reproduction
With a global config already containing an
apiKey, i.e. aftervt login:The
apiKeyandrefreshTokenwere never typed; they came from the global config. That command is the documented example from the README's config section.Cause
configSetCmdbases its write on a merged read (src/cmd/lib/config.ts:138):loadConfig()is correct as a read — callers want global-with-local-on-top. The bug is using that resolved view as the basis for a scoped write.saveLocalConfigvalidates and stringifies whatever it's handed, so the inherited values land in the file as if they'd been set locally.The existing mitigation doesn't fire.
offerToAddToGitignore()is gated onkey === "apiKey" && !useGlobal(config.ts:159), so it only prompts when someone deliberately sets a local key. The leak happens on any local set, silently. This is the concern raised in #223 ("I'm a bit concerned about people mistakenly commiting their local configs with their apiKey to git... Any ideas here?") — the mechanism turns out not to require the user to have typed the key at all.Symmetric case: the same merged object flows into
saveGlobalConfigon theuseGlobalbranch, so a globalvt config setrun inside a Val with local overrides promotes those overrides into the global config permanently. (Read off the code path; I didn't test this one to avoid clobbering my real global config.)Possible fix — a scoped write should read only the scope it writes.
VTConfighas no single-scope loader today (only mergedloadConfig, path getters, and the two savers), so it'd need something likeloadLocalConfig()/loadGlobalConfig()that parses one file without merging;configSetCmdthen picks its base by scope.loadConfig()stays as-is for readers. Happy to open a PR if that direction sounds right — didn't want to presume, given #223 intentionally simplified this area.Verified against upstream/main at 1eb6413, so the line numbers should match what a maintainer sees. Ping me with any changes, and drop the issue number here once it's up if you want me to pick up the fix.