[BUG] Custom theme "custom:<slug>" in settings.json not applied at session startup (v2.1.132) — falls back to auto

Resolved 💬 1 comment Opened May 11, 2026 by jacekleja Closed Jun 10, 2026

Bug Description

theme: "custom:<slug>" in ~/.claude/settings.json is not honored at session startup in v2.1.132. The startup read-path falls back to auto even though:

  1. The settings.json schema accepts "custom:bloom" as a valid theme value.
  2. The custom theme file at ~/.claude/themes/bloom.json is valid and loadable.
  3. The /theme picker writes "custom:bloom" back to settings.json correctly when selected manually.
  4. Manually selecting the custom theme via /theme mid-session works — only the startup read-path is broken.

This means the schema-write-read pipeline is split: the writer (picker) and the schema both accept custom:<slug>, but the resolver invoked at startup does not.

Environment Info

  • Platform: Linux (WSL2, Ubuntu 24.04.1 LTS, kernel 6.6.87.2-microsoft-standard-WSL2)
  • Terminal: WarpTerminal (TERM=xterm-256color, COLORTERM=truecolor)
  • Claude Code version: 2.1.132
  • Other installed versions on disk: 2.1.129, 2.1.131 (not active)
  • Settings scope tested: user-level ~/.claude/settings.json. No project-level theme override, no managed settings, no enterprise policy.

Steps to Reproduce

  1. Create ~/.claude/themes/bloom.json with a valid named custom theme:
{
  "name": "Bloom",
  "base": "dark",
  "overrides": {
    "claude": "#ff70be",
    "text":   "#f5e8f0",
    "inactive": "#3a2240"
  }
}
  1. Hand-edit ~/.claude/settings.json to add:
"theme": "custom:bloom"
  1. Quit Claude Code completely (pkill -f claude to ensure no daemon).
  2. Launch a fresh Claude Code session.
  3. Run /theme in the new session.

Expected Behavior

/theme reports the active theme as Bloom (the custom theme named in settings.json). UI chrome reflects the Bloom palette immediately on launch.

Actual Behavior

/theme reports the active theme as auto. UI chrome is the default auto palette. The custom theme is silently ignored.

If the user then manually navigates to Bloom inside the /theme picker and selects it, the second /theme correctly reports Using custom theme Bloom. The picker also writes "theme": "custom:bloom" back to settings.json (idempotent — already there). On next session start, the same problem recurs.

Evidence

  1. Schema accepts "custom:bloom". Hand-editing settings.json triggers the schema validator. Adding any unrecognized field (e.g., "activeCustomTheme": "bloom") is rejected with "Unrecognized field." But "theme": "custom:bloom" is accepted without error — the theme schema has two anyOf branches: the closed enum of built-ins (auto/dark/light/light-daltonized/dark-daltonized/light-ansi/dark-ansi) and an open {} branch that accepts any string. So custom:bloom is structurally valid at write time.
  1. Picker writes settings.json correctly. After manually selecting Bloom via /theme:
  • ~/.claude/settings.json line 58 reads "theme": "custom:bloom", — exact picker-canonical form.
  • mtime updates to the moment of selection (23:23 in our case, ~30 min after the bloom.json file was created at 22:47).
  1. bloom.json is loadable and valid. Same file is read successfully and applied when selected mid-session via /theme.
  1. Ruled-out causes. None of the following explain the startup-revert:
  • Plugins: disabled all (including warp@claude-code-warp), behavior unchanged. No plugin touches the theme key.
  • Environment variables: no CLAUDE_THEME or similar set.
  • Project-level settings: .claude/settings.json in the project root has no theme field.
  • Managed/policy settings: none configured.
  • ~/.claude.json: zero theme keys present before or after picker writes — the picker only writes to ~/.claude/settings.json.

Internal hints from version 2.1.132 binary

Strings extracted from /home/fanderman/.local/share/claude/versions/2.1.132:

  • Settings-side identifiers: themeSetting, setThemeSetting
  • Runtime-state identifiers: activeCustomTheme, currentTheme, resolvedTheme
  • Loader identifiers: customThemes, reloadCustomThemes, themesPath, themesPaths
  • A [theme] watcher error: log surface (suggesting schema-validation failures during custom-theme load may be silently swallowed at startup)
  • Theme-resolution priority chain ordered: legacyGlobalConfig → default → userSettings → fallback auto

If startup loads custom themes after it resolves the user-settings theme value, then custom:bloom would resolve to nothing → fall back to auto. A reloadCustomThemes call during picker invocation would explain why manual selection works mid-session. This is consistent with the observed behavior but is inference, not a reproduction step.

Related issues

  • #50950 (closed, 2026-04-20) — Feature request that introduced custom themes (v2.1.118).
  • #52190 (closed, 2026-04-23) — Docs PR landing ~/.claude/themes/ and custom:<slug> workflow.
  • #57036 (open, 2026-05-07, v2.1.132) — Custom-themes override fields not applying within a custom theme. Different surface (override fields, not startup-resolve).
  • #6962 (closed, v1.0.98) — Earlier-era issue: theme field not in schema at all. Different bug pattern, fixed; now schema accepts the field but read-path is broken.

Impact

For users hand-editing ~/.claude/themes/<slug>.json and persisting theme: "custom:<slug>" in settings, every new session starts with the wrong theme and requires a manual /theme selection step. Workflows that rely on settings.json as source-of-truth (dotfile sync, multi-machine setups, plugin-distributed themes via ~/.claude/themes/) are silently broken.

The workaround — running /theme once per session and selecting the desired custom theme — defeats the purpose of having the theme persisted in settings.json.

Suggested triage direction

Likely a single ordering bug in the startup theme-resolver: load ~/.claude/themes/*.json before resolving the user-settings theme string against the custom-themes registry. Currently the resolver appears to evaluate custom:bloom against an empty customThemes map, falling through to auto.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗