Repository navigation
kcap crashes with an unhandled exception when config.json omits optional keys #435
Description
Problem
A ~/.config/kcap/config.json that omits optional top-level keys aborts every kcap command with an unhandled exception and a native-crash exit — before any command dispatches, including the kcap config set a user would need to repair the file.
Reproduce with a minimal hand-written config:
{
"version": 2,
"active_profile": "test",
"profiles": { "test": { "server_url": "https://example.test" } }
}$ kcap profile list
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at Capacitor.Cli.Core.Config.ProfileResolver.Resolve() + 0xac
at Capacitor.Cli.Core.Config.AppConfig.<ResolveServerUrl>d__13.MoveNext() + 0x1b0
It is not one crash site. Each missing key fails somewhere different:
| Missing key | Failure |
|---|---|
profile_bindings |
NullReferenceException at ProfileResolver.cs:78 |
profiles |
NullReferenceException at AppConfig.NormalizeProfileVisibilities — before the resolver runs |
active_profile |
ArgumentNullException at ProfileResolver.ResolveByName |
Root cause
Every property on ProfileConfig/Profile/DaemonSettings/CwdRemap is init-only. The System.Text.Json source generator cannot use init-only setters, so it emits ObjectCreator = null and constructs the record through a synthetic object initializer instead:
ObjectWithParameterizedConstructorCreator = static args => new ProfileConfig() {
Version = (int)args[0], ActiveProfile = (string)args[1], Profiles = (…)args[2],
ProfileBindings = (…)args[3], CwdRemap = (…)args[4], MachineId = (string)args[5] }For any key absent from the payload args[i] is default(T). The member initializers (= new(), = "default", = true) run in the constructor and are then overwritten with null/false/0.
The same quirk also causes silent misbehaviour, not just crashes:
- absent
update_check→false, so update checks are quietly disabled - absent
daemon.max_agents→0, whichDaemonRunner.cs:110propagates intoMaxConcurrentAgents, clamping the daemon to zero agents
A related hazard sits next to it: the v1/v2 routing check node["version"]?.GetValue<int>() is 2 throws an uncaught InvalidOperationException on "version": "2", and any non-2 value falls through to the v1 migration — which rebuilds the config from flat fields a v2 file does not have and persists the result, silently dropping the user's profiles.
Why it matters beyond tests
Found while building the KCAP_CONFIG_DIR-isolated integration fixture in #433: hand-writing a minimal config.json crashed every subsequent kcap invocation, and the fixture had to work around it by seeding config through the CLI instead of writing JSON directly.
Beyond tests, any hand-edited, partially-written (interrupted write), or older-version config file bricks the CLI with an opaque crash rather than a readable error.
Fix shape
ConfigMigration.MigrateIfNeeded is the only production deserialization site for ProfileConfig, so restore the declared defaults there across the whole record graph rather than defensively at each call site — a ProfileResolver-only fix would leave AppConfig, kcap use, and kcap profile remove still crashing. The raw JsonObject is needed for the two defaults that differ from default(T), since only the JSON distinguishes "absent" from an explicit false/0.
Activity
linear-code commentedon Aug 2, 2026
AI-1702
Restore config defaults the JSON source generator discards (#436)