Skip to content

fix(desktop): stop PUT /api/config storing its own transport envelope - #823

Merged
agentforce314 merged 1 commit into
mainfrom
fix/desktop-config-envelope
Aug 9, 2026
Merged

fix(desktop): stop PUT /api/config storing its own transport envelope#823
agentforce314 merged 1 commit into
mainfrom
fix/desktop-config-envelope

Conversation

@agentforce314

Copy link
Copy Markdown
Owner

A real ~/.clawcodex/config.json had a full stale snapshot nested under a config key, with the live stt (voice transcription) block stranded inside it — so the backend, which reads the top level, never saw the configured voice settings.

{
  default_provider, providers, session, settings, env, projects, logoColor,
  config: {            ← envelope stored as data
    default_provider, providers, session, settings, projects, logoColor,
    stt: { enabled: true, provider: "openai", ... }   ← stranded here
  }
}

The nested block was identifiable as a redacted GET /api/config response (providers carrying only base_url/default_model, no env), and stale — its openai provider still pointed at the endpoint the user had since changed.

Why it stuck

saveClawCodexConfig sends {"config": {...}}, and the settings panel autosaves the whole record it last read. So one mis-nested write is copied forward on every later save, and _deep_merge only ever adds — nothing could ever remove it. Worse, the envelope shadows real settings: anything written inside it is invisible to a backend that reads the top level.

config is not a key in the config schema, so its presence can only ever be an envelope stored by mistake.

Fix

  • Unwrap repeatedly on the way in. A double wrap is the shape that does the damage: unwrapping once leaves a config key behind, which then merges in as data.
  • Drop the envelope when saving, so a config that already picked one up is repaired by its next ordinary write — no migration step.
  • Strip it on the way out, so a stored envelope can't be round-tripped straight back by the renderer's whole-record autosave.

Credentials still survive the redacted round trip — that is what the deep-merge is for, and there is a test asserting an api_key is not lost.

Testing

New tests/server/test_desktop_config_envelope.py: single and double unwrap, bare records untouched, a wrapped save landing at the top level with credentials intact, and an existing envelope repaired by the next save. Full Python suite: 9912 passed, 15 skipped.

🤖 Generated with Claude Code

A real config had a full stale snapshot nested under a `config` key, with
the live `stt` (voice transcription) block stranded inside it — so the
backend, which reads the top level, never saw the user's voice settings.

The renderer sends `{"config": {...}}` and autosaves the WHOLE record it
last read, so one mis-nested write is copied forward on every later save,
and `_deep_merge` only ever adds — nothing could ever remove it. `config` is
not a key in the config schema (the file holds default_provider / providers /
session / settings / env / projects / … at the top level), so its presence
can only ever be an envelope stored by mistake.

- Unwrap repeatedly on the way in. A DOUBLE wrap is the shape that does the
  damage: unwrapping once leaves a `config` key that then merges in as data.
- Drop the envelope when saving, so a config that already picked one up is
  repaired by its next ordinary write rather than needing a migration.
- Strip it on the way out too, so a stored envelope can't be round-tripped
  straight back by the renderer's whole-record autosave.

Credentials still survive the redacted round trip — that is what the
deep-merge is for, and there is a test for it.

Co-Authored-By: Claude <noreply@anthropic.com>
@agentforce314
agentforce314 merged commit f996d05 into main Aug 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant