fix(config): leave an unparseable global config untouched (#1876)
* fix(config): leave an unparseable global config untouched
A typo in config.json made getGlobalConfig() fall back to defaults, which telemetry read as consent: any command, even a read-only list, minted a new anonymous id and wrote it over the whole file, dropping a telemetry.enabled false opt-out and every other setting. config set, unset and profile likewise saved the defaults over it.
saveGlobalConfig() and telemetry's writeConfig() now refuse to overwrite a file they cannot parse, telemetry and the update check treat such a file as opted out, and config set, unset and profile exit with an error pointing to config edit. config reset --all can still replace the file, and the existing warning is unchanged.
* fix(config): treat a non-object global config as unreadable
Valid JSON that is not an object (null, an array, a string) also makes
getGlobalConfig() fall back to defaults, silently, so `config set` still
saved those defaults over the user's file. isGlobalConfigUnreadable() now
reports such a file as unreadable, which keeps telemetry off and routes
every save through the same refusal as a parse failure. This matches how
completion-tip already treats a non-object config.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(config): document the refusal to rewrite an unparseable config
docs-lab/reference/cli.md said `config unset` always exits 0. With an
unparseable global config, `config set`, `config unset` and
`config profile` now exit 1 and leave the file unchanged; say so, show
the message and the two fixes, and note telemetry and the update check
stay off until it is fixed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(config): warn about an unparseable global config once per command
Telemetry, the update check and the command each read the global config,
and now that none of them rewrites the broken file, the "Invalid JSON"
warning printed two or three times per command. Warn once per path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(migration): skip profile migration for a config that is not a JSON object
A global config holding [] reached saveGlobalConfig, which now refuses
it, so init and update failed. null already crashed on a property read.
migrateIfNeeded now skips such a file, as it does for a parse failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(telemetry): refuse to write over a non-object global config
The telemetry writer had its own notion of an unreadable config: only a
JSON parse failure counted. Valid JSON that is not an object slipped
through, so updateTelemetryConfig() merged into it and replaced the file
-- an array, a number or a boolean became a bare telemetry object, a
string spread into numeric character keys, and null threw a TypeError
instead of the actionable refusal every other writer reports.
Funnel both notions through one predicate: isConfigRootObject() in
core/global-config.ts now backs isGlobalConfigUnreadable() and the
telemetry reader, so every shape the global guard rejects is classified
invalid on read and refused on write. Both writers report the same
one-line message via unreadableGlobalConfigMessage().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(config): read a non-object global config as plain defaults
getGlobalConfig() spread the parsed root into its result before the
unreadable predicate was consulted, so the shape of the root leaked to
every caller: a config of "abc" returned defaults plus the numeric
character keys 0, 1 and 2. Check isConfigRootObject() right after
parsing and answer with plain defaults, as for a file that did not parse
at all.
Reported by CodeRabbit as an outside-the-diff finding.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(config): stop `config list` crashing on a null config root
`config list` re-reads the raw file to mark each value explicit or
default, and assigned JSON.parse() straight to rawConfig. A root of
`null` then crashed the command with a TypeError stack trace, the one
failure mode this PR is meant to remove, and it did so on a read-only
command. Normalize a non-object root to {} through the shared
isConfigRootObject() predicate so the listing shows plain defaults.
Reported by CodeRabbit as an outside-the-diff finding.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(config): show the telemetry notice before the first-run write check
Since #1835, nothing is tracked until the notice has been shown, so the
first-run test must show it before tracking the command.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com> D
Dwin Gharibi committed
605d9e7a2bb5c1bab90268933f9b84ff1eb8807c
Parent: 626269e
Committed by GitHub <noreply@github.com>
on 9/16/2026, 7:24:40 PM