[BUG] Custom theme "custom:<slug>" in settings.json not applied at session startup (v2.1.132) — falls back to auto
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:
- The settings.json schema accepts
"custom:bloom"as a validthemevalue. - The custom theme file at
~/.claude/themes/bloom.jsonis valid and loadable. - The
/themepicker writes"custom:bloom"back to settings.json correctly when selected manually. - Manually selecting the custom theme via
/thememid-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
- Create
~/.claude/themes/bloom.jsonwith a valid named custom theme:
{
"name": "Bloom",
"base": "dark",
"overrides": {
"claude": "#ff70be",
"text": "#f5e8f0",
"inactive": "#3a2240"
}
}
- Hand-edit
~/.claude/settings.jsonto add:
"theme": "custom:bloom"
- Quit Claude Code completely (
pkill -f claudeto ensure no daemon). - Launch a fresh Claude Code session.
- Run
/themein 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
- 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 — thethemeschema 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. Socustom:bloomis structurally valid at write time.
- Picker writes settings.json correctly. After manually selecting Bloom via
/theme:
~/.claude/settings.jsonline 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).
- bloom.json is loadable and valid. Same file is read successfully and applied when selected mid-session via
/theme.
- 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_THEMEor similar set. - Project-level settings:
.claude/settings.jsonin the project root has nothemefield. - 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/andcustom:<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:
themefield 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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗