Skip to content

kcap crashes with an unhandled exception when config.json omits optional keys #435

Closed as not planned

Description

@alexeyzimarev

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, which DaemonRunner.cs:110 propagates into MaxConcurrentAgents, 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

  1. linear-code commented on Aug 2, 2026

    @linear-code
  2. added a commit that references this issue on Aug 2, 2026
    4556605
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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