claude-code - 💡(How to fix) Fix [BUG] Custom theme overrides silently ignored for markdown-rendered slots (codespan, text, error, warning, success, ...) [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#52465Fetched 2026-04-24 06:06:30
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Author
Participants
Timeline (top)
labeled ×4

Custom themes in ~/.claude/themes/<name>.json accept an overrides map, and the theme loader validates and stores it. However, the markdown renderer bypasses the merged theme and reads colors straight from the hardcoded base palette, so overrides for any slot touched by the renderer are silently ignored.

Most visible symptom: inline code spans (`like this`) always render with the base permission color (light periwinkle rgb(177,185,249) on base: "dark"), regardless of what the user sets for permission in overrides.

Error Message

Expected: inline code renders pure red; warning/success/error tokens render the overridden colors.

Error Messages/Logs

"error": "#00ff00" Expected: inline code renders pure red; warning/success/error tokens render the overridden colors.

  • error

Root Cause

The theme loader correctly builds the validated overrides dict A and keeps it in the theme object:

// theme loader
if (typeof O.overrides === "object" && O.overrides !== null) {
  let z = bx(T);
  for (let [Y, w] of Object.entries(O.overrides))
    if (Object.hasOwn(z, Y) && UDH(w)) A[Y] = w;
}
return { slug: H, name: $, base: T, overrides: A, source: q };

React components consume a merged theme:

L = wD.useMemo(() => K99(bx(G), A ?? W?.overrides), [G, A, W]);

But the markdown renderer calls the color helper a8 with the base-name string, not the merged theme:

// markdown renderer
case "codespan": return a8("permission", _)(H.text);
//                         ^^^^^^^^^^^^^ _  is the base name ("dark"), not the merged theme

And a8 itself looks up the value from the hardcoded base palette only:

function a8(H, _, q = "foreground") {
  return (K) => {
    if (!H) return K;
    if (H.startsWith("rgb(") || H.startsWith("#") || H.startsWith("ansi256(") || H.startsWith("ansi:"))
      return LKH(K, H, q);
    return LKH(K, bx(_)[H], q);   //  ←  bx(baseName)[slot]  — merged overrides never consulted
  };
}

function bx(H) {
  switch (H) {
    case "light":             return va4;
    case "light-ansi":        return Na4;
    case "dark-ansi":         return Va4;
    case "light-daltonized":  return ha4;
    case "dark-daltonized":   return Sa4;
    default:                  return ya4;   // "dark"
  }
}

So the merged overrides A are built but never threaded into the markdown renderer's color lookup path. All a8(slot, baseName) call-sites silently fall through to the base palette.

Fix Action

Workaround

None via the theme file — every affected slot is hardcoded per base. The only partial mitigation is switching base: "dark" → base: "dark-ansi", which makes the renderer pull ansi:blueBright etc. from the terminal's ANSI palette. If you already run a tokyonight/solarized/etc. terminal palette it'll look coherent, but you still can't tweak individual slots from the theme file.

Code Example



---

{
      "name": "test",
      "base": "dark",
      "overrides": {
        "permission": "#ff0000",
        "text": "#ff0000",
        "warning": "#ff0000",
        "success": "#ff0000",
        "error": "#00ff00"
      }
    }

---

// theme loader
if (typeof O.overrides === "object" && O.overrides !== null) {
  let z = bx(T);
  for (let [Y, w] of Object.entries(O.overrides))
    if (Object.hasOwn(z, Y) && UDH(w)) A[Y] = w;
}
return { slug: H, name: $, base: T, overrides: A, source: q };

---

L = wD.useMemo(() => K99(bx(G), A ?? W?.overrides), [G, A, W]);

---

// markdown renderer
case "codespan": return a8("permission", _)(H.text);
//                         ^^^^^^^^^^^^^ _  is the base name ("dark"), not the merged theme

---

function a8(H, _, q = "foreground") {
  return (K) => {
    if (!H) return K;
    if (H.startsWith("rgb(") || H.startsWith("#") || H.startsWith("ansi256(") || H.startsWith("ansi:"))
      return LKH(K, H, q);
    return LKH(K, bx(_)[H], q);   //  ←  bx(baseName)[slot]  — merged overrides never consulted
  };
}

function bx(H) {
  switch (H) {
    case "light":             return va4;
    case "light-ansi":        return Na4;
    case "dark-ansi":         return Va4;
    case "light-daltonized":  return ha4;
    case "dark-daltonized":   return Sa4;
    default:                  return ya4;   // "dark"
  }
}
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code

What's Wrong?

Summary

Custom themes in ~/.claude/themes/<name>.json accept an overrides map, and the theme loader validates and stores it. However, the markdown renderer bypasses the merged theme and reads colors straight from the hardcoded base palette, so overrides for any slot touched by the renderer are silently ignored.

Most visible symptom: inline code spans (`like this`) always render with the base permission color (light periwinkle rgb(177,185,249) on base: "dark"), regardless of what the user sets for permission in overrides.

Version

  • Claude Code: 2.1.118 (also reproduces on 2.1.117)
  • Platform: macOS 14 (Kitty + tmux + sidekick.nvim, but also reproduces bare in Warp)

Suggested fix

Thread the resolved theme (or at minimum the merged palette K99(bx(base), overrides)) through the markdown renderer entry point, and have a8 read from that merged palette rather than bx(baseName). The existing L computed for React would work — it just needs to be passed into the markdown renderer so every a8(slot, _) resolves against the merged palette.

Workaround

None via the theme file — every affected slot is hardcoded per base. The only partial mitigation is switching base: "dark" → base: "dark-ansi", which makes the renderer pull ansi:blueBright etc. from the terminal's ANSI palette. If you already run a tokyonight/solarized/etc. terminal palette it'll look coherent, but you still can't tweak individual slots from the theme file.

What Should Happen?

Expected: inline code renders pure red; warning/success/error tokens render the overridden colors. Actual: all of the above render in the base "dark" palette's colors, as if overrides were empty. background, bashBorder, diffAdded*, userMessageBackground*, mode badges, and other chrome slots do honor overrides correctly — so it's not a file-loading issue.

Error Messages/Logs

Steps to Reproduce

Reproduction

  1. Create ~/.claude/themes/test.json:
    {
      "name": "test",
      "base": "dark",
      "overrides": {
        "permission": "#ff0000",
        "text": "#ff0000",
        "warning": "#ff0000",
        "success": "#ff0000",
        "error": "#00ff00"
      }
    }
  2. /theme → select "test" → reopen session.
  3. Ask Claude to produce a reply containing inline code, a warning message, etc.

Expected: inline code renders pure red; warning/success/error tokens render the overridden colors. Actual: all of the above render in the base "dark" palette's colors, as if overrides were empty. background, bashBorder, diffAdded*, userMessageBackground*, mode badges, and other chrome slots do honor overrides correctly — so it's not a file-loading issue.

Root cause

The theme loader correctly builds the validated overrides dict A and keeps it in the theme object:

// theme loader
if (typeof O.overrides === "object" && O.overrides !== null) {
  let z = bx(T);
  for (let [Y, w] of Object.entries(O.overrides))
    if (Object.hasOwn(z, Y) && UDH(w)) A[Y] = w;
}
return { slug: H, name: $, base: T, overrides: A, source: q };

React components consume a merged theme:

L = wD.useMemo(() => K99(bx(G), A ?? W?.overrides), [G, A, W]);

But the markdown renderer calls the color helper a8 with the base-name string, not the merged theme:

// markdown renderer
case "codespan": return a8("permission", _)(H.text);
//                         ^^^^^^^^^^^^^ _  is the base name ("dark"), not the merged theme

And a8 itself looks up the value from the hardcoded base palette only:

function a8(H, _, q = "foreground") {
  return (K) => {
    if (!H) return K;
    if (H.startsWith("rgb(") || H.startsWith("#") || H.startsWith("ansi256(") || H.startsWith("ansi:"))
      return LKH(K, H, q);
    return LKH(K, bx(_)[H], q);   //  ←  bx(baseName)[slot]  — merged overrides never consulted
  };
}

function bx(H) {
  switch (H) {
    case "light":             return va4;
    case "light-ansi":        return Na4;
    case "dark-ansi":         return Va4;
    case "light-daltonized":  return ha4;
    case "dark-daltonized":   return Sa4;
    default:                  return ya4;   // "dark"
  }
}

So the merged overrides A are built but never threaded into the markdown renderer's color lookup path. All a8(slot, baseName) call-sites silently fall through to the base palette.

Affected slots

Every slot looked up via a8(slot, ...) in the markdown renderer. Grep of the binary finds:

  • claude
  • error
  • fastMode
  • inactive
  • permission (← used for codespan / inline code)
  • promptBorder
  • success
  • suggestion
  • text
  • warning

Slots applied through React props (L/K99) — e.g. background, bashBorder, userMessageBackground*, diffAdded*, rate_limit_, clawd_, mode badges, subagent colors — are unaffected and honor overrides correctly.

Claude Model

Opus

Is this a regression?

No, this never worked

Last Working Version

No response

Claude Code Version

2.1.118 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other

Additional Information

No response

extent analysis

TL;DR

The most likely fix is to thread the resolved theme through the markdown renderer entry point, allowing it to read from the merged palette instead of the hardcoded base palette.

Guidance

  • Identify all call-sites of a8(slot, baseName) in the markdown renderer and modify them to use the merged theme instead of the base name.
  • Update the a8 function to accept the merged theme as an argument, and use it to look up the color values.
  • Verify that the merged theme is correctly passed to the markdown renderer and that the a8 function is using it to resolve color values.
  • Test the changes with different themes and overrides to ensure that the issue is fully resolved.

Example

// updated a8 function
function a8(slot, mergedTheme) {
  return (text) => {
    if (!slot) return text;
    return LKH(text, mergedTheme[slot], "foreground");
  };
}

// updated markdown renderer
case "codespan": return a8("permission", mergedTheme)(text);

Notes

This fix assumes that the mergedTheme object is correctly computed and passed to the markdown renderer. Additional debugging may be necessary to ensure that the mergedTheme object is correctly populated with the overridden values.

Recommendation

Apply the workaround of switching to base: "dark-ansi" to mitigate the issue, but ultimately apply the suggested fix to fully resolve the problem, as it allows for more flexibility and customization of the theme.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING