Skip to content

tui: one invalid cli.json field silently discards the entire config #56318

Open

Description

@neoscorpi0

tui: one invalid cli.json field silently discards the entire config

Summary

A single invalid field in ~/.config/opencode/cli.json causes OpenCode to silently
drop the whole file. No error is shown; every setting (sidebar, cursor, theme,
keybinds, …) falls back to defaults. This is very hard to diagnose because there is
no user-visible signal that validation failed.

Environment

  • opencode version: 2.0.32 (release / latest, single binary)
  • OS: Linux 7.1.8-arch1-3 x86_64 (Arch)
  • Terminal: xterm-256color (also reproduced under tmux 3.7b / tmux-256color); occurs both inside and outside tmux
  • Shell: /usr/bin/bash
  • Install/channel: release, latest
  • Active plugins: none configured (only built-in opencode.sidebar.* plugins)

Reproduction

  1. Create ~/.config/opencode/cli.json containing a valid setting plus one invalid
    field — e.g. a top-level leader given as a string (the V1 tui.json shape):

    {
      "$schema": "https://opencode.ai/v2/cli.json",
      "leader": "ctrl+x",
      "session": { "sidebar": "hide" },
      "cursor": { "style": "line", "blinking": false }
    }
  2. Launch the TUI (opencode).

  3. Observe that the sidebar is still visible and the cursor is a block, and that
    custom keybinds are ignored.

Expected Behavior

Either the config is applied, or OpenCode surfaces a visible error/warning naming the
invalid field (and ideally still applies the valid parts).

Actual Behavior

The entire cli.json is discarded and every setting reverts to defaults. No error is
shown to the user. The only way to notice is by observing that settings silently do
nothing.

Confirmed by inspecting the emitted terminal escape sequences: with the file above the
TUI emits a blinking block cursor (ESC [ 1 SPACE q); a minimal valid file emits
the steady line cursor (ESC [ 6 SPACE q). Bisecting field-by-field isolates
top-level leader as the trigger.

Additional Context

  • The top-level leader schema expects an object ({ "timeout": <int> }); it is not
    the leader key. The leader key belongs at keybinds.leader. A string value fails
    schema validation and invalidates the whole document.
  • Source: packages/opencode/src/config/tui.ts → on validation failure the loader runs
    Effect.logWarning("skipping invalid tui config", …) and returns {}. That warning is
    not surfaced in the TUI, so the failure is invisible to the user.
  • Workaround: move the value to keybinds.leader (and/or drop the invalid key). The file
    then loads correctly.
  • Impact: high discoverability cost; users assume the feature is broken (e.g. "sidebar
    won't hide", "cursor won't change") when in fact their config was never read.

Suggested Fix

Surface a visible warning when cli.json fails validation (e.g. a transient TUI notice
or a line in /debug), and/or report the offending key path, rather than silently
discarding the entire file.

Activity

  1. opencode-agent commented on Oct 17, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I was able to reproduce this on 2.0.32, and it still happens on the latest dev (bc5fa1c).

    Steps:

    1. Write a cli.json with session.sidebar: "hide", a cursor setting, and a top-level "leader": "ctrl+x".
    2. Start the TUI and send a prompt. The sidebar is still shown, and no warning appears.
    3. Remove only the leader line and start again. The sidebar is hidden as configured.

    The recording and screenshots below show both runs, and the script below reproduces it.

    The v2 loader validates cli.json as a whole and falls back to empty settings if any field is invalid, without telling you. Your workaround (moving the leader key to keybinds.leader) is the right one for now.

    Related:

    recording.mp4

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#56318: one invalid cli.json field silently discards the whole file.
    // TUI 1: cli.json has session.sidebar "hide" + an invalid top-level `leader: "ctrl+x"` -> sidebar still shown, no warning.
    // TUI 2: same file without `leader` -> sidebar hidden as configured.
    import { Effect } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    import { existsSync, readFileSync, writeFileSync } from "node:fs"
    import { join } from "node:path"
    
    const valid = {
      session: { sidebar: "hide" },
      cursor: { style: "line", blinking: false },
    }
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        project: { git: true },
        setup: ({ tuiConfig }) =>
          Effect.sync(() => {
            Object.assign(tuiConfig as Record<string, unknown>, valid, { leader: "ctrl+x" })
          }),
        tui: { recording: true, viewport: { cols: 160, rows: 40 } },
      },
      ({ ui, llm, tuis, artifacts }) =>
        Effect.gen(function* () {
          yield* llm.queue(Llm.text("Hello from the invalid-config run."))
          yield* ui.submit("hello")
          yield* ui.waitFor("Hello from the invalid-config run.")
          yield* Effect.sleep(1500)
          yield* ui.screenshot("1-invalid-leader-sidebar-still-visible")
    
          // Same config minus the invalid `leader` field
          const file = join(artifacts, "files", ".opencode", "cli.json")
          if (!existsSync(file)) return yield* Effect.die(new Error(`missing ${file}`))
          yield* Effect.log(`cli.json used by TUI 1:\n${readFileSync(file, "utf8")}`)
          writeFileSync(file, JSON.stringify(valid, null, 2))
          const second = yield* tuis.launch({ recording: true, viewport: { cols: 160, rows: 40 } })
          yield* llm.queue(Llm.text("Hello from the valid-config run."))
          yield* second.ui.submit("hello")
          yield* second.ui.waitFor("Hello from the valid-config run.")
          yield* Effect.sleep(1500)
          yield* second.ui.screenshot("2-valid-sidebar-hidden")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions