Skip to content

[bug] vt config set --local copies global apiKey/refreshToken into .vt/config.yaml #280

Description

@nbbaier

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions