SLOPSHOPPER

skins

Colour skins for all of Claude Code (Noir, Tokyo Night, Dracula, Catppuccin, Gruvbox, Everforest, Nordic, Monokai) as custom themes, picked from a pane

newpanecommandtoast
v0.1.0no licenseupdated 2026-10-10ianwilson97/claude-mods/skins
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · skins
│ ┃ Skins ✕ › fix the failing auth test and add an audit log call │ ┃ Skins │ ┃ Pick one to recolour tool rows, borders, ⏺ Read(src/auth.ts) │ ┃ diffs and the spinner. ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ ╭──────────────────╮╭──────────────────╮╭── ⎿ Added 2 lines, removed 1 line │ ┃ │ Noir ││ Tokyo Night ││ D ⏺ Bash(bun test) │ ┃ │ ✎ Edit units.js ││ ✎ Edit units.js ││ ✎ ⎿ 3 pass, 1 fail │ ┃ │ ›_ Run tests ││ ›_ Run tests ││ › │ ┃ │ ● ● ● ● ● ● ││ ● ● ● ● ● ● ││ ● ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ ╰──────────────────╯╰──────────────────╯╰── │ ┃ [ Use Noir ] [ Use Tokyo Night ] [ U ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ current: default › /skins │ ⎿ skins: Skins open. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Skins
Skins Pick one to recolour tool rows, borders, diffs and the spinner. ╭─────────────────────────╮╭─────────────────────────╮╭──── │ Noir ││ Tokyo Night ││ Dra │ ✎ Edit units.js ││ ✎ Edit units.js ││ ✎ E │ ›_ Run tests ││ ›_ Run tests ││ ›_ │ ● ● ● ● ● ● ││ ● ● ● ● ● ● ││ ● ● ╰─────────────────────────╯╰─────────────────────────╯╰──── [ Use Noir ] [ Use Tokyo Night ] [ Use current: default
README

ianwilson-mods

Claude Code mods (function-hook plugins). Each folder is one plugin; .claude-plugin/marketplace.json makes this repo a marketplace.

ModWhat it does
cheap-worker/worker spins up Claude Code workers from a picker; /workers lists, messages and closes them. Team mode lets workers find each other (ListAgents) and talk (SendMessage).
agent-progress/progress: animated progress bar per agent (main, subagents, workers) from their task lists.
agent-map/map: project file tree; red 🔨 = an agent is editing it, green ✓ = done, ready for review.
context-barContext-window breakdown above the prompt.
usage-meter5-hour / weekly usage in the footer.
quick-settings⚙ footer button for model and effort.
skins/skins: colour themes (Tokyo Night, Dracula, Gruvbox, …).
cw-pane, driver-seat-guard, evidence-bandFor the driver-seat workflow; skip them elsewhere.

Install

/plugin marketplace add ianwilson97/claude-mods
/plugin install cheap-worker@ianwilson-mods
/plugin install agent-progress@ianwilson-mods
/plugin install agent-map@ianwilson-mods

On the machine where this folder is the live copy (loaded through CLAUDE_CODE_PLUGIN_DIRS), don't also install from the marketplace: each mod would load twice.

cheap-worker: two switches

Set per machine in /config (or pluginConfigs.cheap-worker.options in ~/.claude/settings.json):

OptionValues
provideropenrouter (default)Any OpenRouter model; key read from $CW_OPENROUTER_API_KEY (export it in ~/.zshenv). Workers skip user settings, hooks and plugins.
bedrockClaude only (Sonnet / Opus / Haiku). Workers keep your user settings, so the Bedrock env there (CLAUDE_CODE_USE_BEDROCK, AWS_REGION, AWS_PROFILE, …) applies. No call to openrouter.ai.
launchpanes (default)All workers tiled in tmux session cw: tmux new -A -s cw.
tabsEach worker gets a tmux session of its own and opens in a background tab: a cmux tab when Claude Code runs inside cmux, else a Ghostty tab (focus stays where you were).

Work laptop: provider = bedrock, launch = tabs.

Reaching a worker: crew

The mod installs crew into ~/.local/bin (that folder must be on your PATH; it never overwrites a different crew).

crew            # list running workers, pick one by number
crew ab12c      # attach to cw-ab12c

If Ghostty can't be scripted (macOS asks once to let Claude control Ghostty; a managed Mac may refuse), the worker still runs: press CMD+T and run crew. Closing a tab only detaches; the worker keeps running until you close it from /workers.

Needs: tmux; for auto-opened tabs, cmux or Ghostty 1.3+.

First run on a new machine: checks

  1. /plugin lists the mods as enabled. Managed settings can block marketplaces or plugins; if so, nothing loads.
  2. /worker, launch one with task say hi; a Ghostty tab opens (or the toast says CMD+T, then crew …).
  3. Turn on team, launch two workers, then from /workers send one a message: messaging is Claude Code's own (ListAgents / SendMessage) and works under Bedrock unless policy turns it off.
Source 3 files
hooks/register.tsx 140 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { SKINS, skinOf, themeFile, themeRef } from './skins'
5import type { Skin } from './skins'
6
7const PANE = 'skins'
8const applied = atom({ plugin: 'skins', key: 'applied' } as const, null as string | null)
9
10// Claude Code reads custom themes from ~/.claude/themes/<slug>.json and watches the folder.
11async function installThemes($: EngineInterface) {
12  const home = await $.env.get('HOME')
13  if (!home) return
14  for (const s of SKINS) {
15    const path = `${home}/.claude/themes/skin-${s.slug}.json`
16    const want = themeFile(s)
17    if ((await $.fs.read(path).catch(() => null)) !== want) await $.fs.write(path, want)
18  }
19}
20
21// /config's theme row only takes the built-in themes, so a custom one is set the way the
22// settings schema allows it: "theme": "custom:<slug>" in ~/.claude/settings.json.
23async function settingsPath($: EngineInterface) {
24  const home = await $.env.get('HOME')
25  return home ? `${home}/.claude/settings.json` : null
26}
27
28async function currentTheme($: EngineInterface): Promise<string | undefined> {
29  const path = await settingsPath($)
30  const raw = path ? await $.fs.read(path).catch(() => null) : null
31  try {
32    const t = typeof raw === 'string' ? (JSON.parse(raw) as { theme?: unknown }).theme : undefined
33    if (typeof t === 'string') return t
34  } catch {
35    // unreadable settings: fall back to /config's view
36  }
37  const row = (await $.config.list().catch(() => [])).find(r => r.key === 'theme')
38  return typeof row?.value === 'string' ? row.value : undefined
39}
40
41/** `value` null removes the key (back to the default theme). Only the theme key changes. */
42export function withTheme(settingsJson: string, value: string | null): string {
43  const d = JSON.parse(settingsJson) as Record<string, unknown>
44  if (value === null) delete d.theme
45  else d.theme = value
46  return `${JSON.stringify(d, null, 2)}\n`
47}
48
49async function apply($: EngineInterface, value: string | null, label: string) {
50  const path = await settingsPath($)
51  const raw = path ? await $.fs.read(path).catch(() => null) : null
52  try {
53    if (!path || typeof raw !== 'string') throw new Error('~/.claude/settings.json not readable')
54    await $.fs.write(path, withTheme(raw, value))
55    await update($, applied, () => value ?? 'dark')
56    $.ui.toast(`Skin: ${label}`)
57  } catch (err) {
58    $.ui.toast(`Couldn't switch skin: ${err instanceof Error ? err.message : String(err)}`)
59  }
60}
61
62export const register: Register = on => {
63  on('session.start', async ($, e, next) => {
64    await $.command.register({ name: 'skins', description: 'Pick a colour skin for Claude Code' })
65    await installThemes($).catch(() => undefined)
66    return next(e)
67  })
68
69  on('command.run', { command: 'skins' }, async $ => {
70    await installThemes($).catch(() => undefined)
71    await $.ui.open({ id: PANE, title: 'Skins' })
72    return { text: 'Skins open.' }
73  })
74
75  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
76    const { Box, Button, Text } = $.ui.resolve(e)
77    const last = await read($, applied) // read every draw, so applying a skin redraws the pane
78    const theme = (await currentTheme($)) ?? last ?? undefined
79    const inUse = skinOf(theme)
80
81    const card = (s: Skin) => {
82      const isOn = inUse?.slug === s.slug
83      return (
84        <Box key={`skin-${s.slug}`} flexDirection="column" width="48%" marginBottom={1}>
85          <Box
86            flexDirection="column"
87            borderStyle="round"
88            borderColor={isOn ? s.accent : s.subtle}
89            backgroundColor={s.bg}
90            paddingX={1}
91          >
92            <Box justifyContent="space-between">
93              <Text bold color={s.fg}>{s.name}</Text>
94              {isOn && <Text color={s.accent}>✓</Text>}
95            </Box>
96            <Text color={s.fg}>
97              <Text color={s.orange}>✎ </Text>
98              <Text bold color={s.orange}>Edit</Text> units.js
99            </Text>
100            <Text color={s.fg}>
101              <Text color={s.yellow}>›_ </Text>
102              <Text bold color={s.yellow}>Run</Text> tests
103            </Text>
104            <Text>
105              {[s.cyan, s.orange, s.yellow, s.purple, s.green, s.red].map((col, i) => (
106                <Text key={`dot-${s.slug}-${i}`} color={col}>● </Text>
107              ))}
108            </Text>
109          </Box>
110          <Button
111            key={`use-${s.slug}`}
112            label={isOn ? 'In use' : `Use ${s.name}`}
113            variant={isOn ? 'secondary' : 'primary'}
114            onPress={() => (isOn ? undefined : apply($, themeRef(s), s.name))}
115          />
116        </Box>
117      )
118    }
119
120    return (
121      <Box flexDirection="column" paddingX={1}>
122        <Text bold>Skins</Text>
123        <Text dimColor>Pick one to recolour tool rows, borders, diffs and the spinner.</Text>
124        <Box flexWrap="wrap" justifyContent="space-between" marginTop={1}>
125          {SKINS.map(card)}
126        </Box>
127        <Box key="default-row" columnGap={1}>
128          <Button
129            key="use-default"
130            plain
131            dimColor={!inUse}
132            label={inUse ? '↺ back to the default dark theme' : `current: ${theme ?? 'default'}`}
133            onPress={() => (inUse ? apply($, null, 'default') : undefined)}
134          />
135        </Box>
136      </Box>
137    )
138  })
139}
140
hooks/skins.ts 61 lines
1/** One skin: a palette that `overridesFor` spreads over every key of Claude Code's theme. */
2export type Skin = {
3  slug: string
4  name: string
5  bg: string; panel: string; fg: string; muted: string; subtle: string; accent: string
6  blue: string; green: string; red: string; yellow: string; purple: string; cyan: string; orange: string; pink: string
7}
8
9export const SKINS: readonly Skin[] = [
10  { slug: 'noir', name: 'Noir', bg: '#121212', panel: '#1f1f1f', fg: '#e6e6e6', muted: '#8a8a8a', subtle: '#4a4a4a', accent: '#ffffff', blue: '#cfcfcf', green: '#d6d6d6', red: '#ff6b6b', yellow: '#e6e6e6', purple: '#bdbdbd', cyan: '#dedede', orange: '#e0e0e0', pink: '#ff8a8a' },
11  { slug: 'tokyo-night', name: 'Tokyo Night', bg: '#1a1b26', panel: '#24283b', fg: '#c0caf5', muted: '#565f89', subtle: '#3b4261', accent: '#ff9e64', blue: '#7aa2f7', green: '#9ece6a', red: '#f7768e', yellow: '#e0af68', purple: '#bb9af7', cyan: '#7dcfff', orange: '#ff9e64', pink: '#ff007c' },
12  { slug: 'dracula', name: 'Dracula', bg: '#282a36', panel: '#343746', fg: '#f8f8f2', muted: '#6272a4', subtle: '#44475a', accent: '#ffb86c', blue: '#8be9fd', green: '#50fa7b', red: '#ff5555', yellow: '#f1fa8c', purple: '#bd93f9', cyan: '#8be9fd', orange: '#ffb86c', pink: '#ff79c6' },
13  { slug: 'catppuccin', name: 'Catppuccin', bg: '#1e1e2e', panel: '#313244', fg: '#cdd6f4', muted: '#6c7086', subtle: '#45475a', accent: '#cba6f7', blue: '#89b4fa', green: '#a6e3a1', red: '#f38ba8', yellow: '#f9e2af', purple: '#cba6f7', cyan: '#89dceb', orange: '#fab387', pink: '#f5c2e7' },
14  { slug: 'gruvbox', name: 'Gruvbox', bg: '#282828', panel: '#32302f', fg: '#ebdbb2', muted: '#928374', subtle: '#504945', accent: '#fe8019', blue: '#83a598', green: '#b8bb26', red: '#fb4934', yellow: '#fabd2f', purple: '#d3869b', cyan: '#8ec07c', orange: '#fe8019', pink: '#d3869b' },
15  { slug: 'everforest', name: 'Everforest', bg: '#2d353b', panel: '#343f44', fg: '#d3c6aa', muted: '#859289', subtle: '#4f585e', accent: '#a7c080', blue: '#7fbbb3', green: '#a7c080', red: '#e67e80', yellow: '#dbbc7f', purple: '#d699b6', cyan: '#83c092', orange: '#e69875', pink: '#d699b6' },
16  { slug: 'nordic', name: 'Nordic', bg: '#2e3440', panel: '#3b4252', fg: '#d8dee9', muted: '#616e88', subtle: '#434c5e', accent: '#88c0d0', blue: '#81a1c1', green: '#a3be8c', red: '#bf616a', yellow: '#ebcb8b', purple: '#b48ead', cyan: '#88c0d0', orange: '#d08770', pink: '#b48ead' },
17  { slug: 'monokai', name: 'Monokai', bg: '#272822', panel: '#3e3d32', fg: '#f8f8f2', muted: '#75715e', subtle: '#49483e', accent: '#a6e22e', blue: '#66d9ef', green: '#a6e22e', red: '#f92672', yellow: '#e6db74', purple: '#ae81ff', cyan: '#66d9ef', orange: '#fd971f', pink: '#f92672' },
18]
19
20const hex = (h: string) => [1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16)) as [number, number, number]
21const rgb = ([r, g, b]: number[]) => `rgb(${r},${g},${b})`
22/** `amount` of `a` over `b`, as Claude Code's theme writes colours. */
23export const mix = (a: string, b: string, amount: number) => {
24  const [x, y] = [hex(a), hex(b)]
25  return rgb(x.map((v, i) => Math.round(v * amount + y[i]! * (1 - amount))))
26}
27const c = (h: string) => rgb(hex(h))
28
29/** Every theme key Claude Code's base theme has, from one palette. */
30export function overridesFor(s: Skin): Record<string, string> {
31  const t: Record<string, string> = {
32    text: c(s.fg), inverseText: c(s.bg), inactive: c(s.muted), inactiveShimmer: mix(s.fg, s.muted, 0.4), subtle: c(s.muted),
33    promptBorder: c(s.muted), promptBorderShimmer: mix(s.fg, s.muted, 0.5),
34    claude: c(s.accent), claudeShimmer: mix(s.fg, s.accent, 0.35), clawd_body: c(s.accent), clawd_background: c(s.bg), briefLabelClaude: c(s.accent),
35    claudeBlue_FOR_SYSTEM_SPINNER: c(s.blue), claudeBlueShimmer_FOR_SYSTEM_SPINNER: mix(s.fg, s.blue, 0.35),
36    permission: c(s.blue), permissionShimmer: mix(s.fg, s.blue, 0.35), suggestion: c(s.blue), ide: c(s.blue), professionalBlue: c(s.blue), briefLabelYou: c(s.blue),
37    remember: c(s.purple), autoAccept: c(s.purple), autoAcceptShimmer: mix(s.fg, s.purple, 0.35), skill: c(s.purple), merged: c(s.purple), effortUltra: c(s.purple),
38    planMode: c(s.cyan), background: c(s.cyan), bashBorder: c(s.pink),
39    success: c(s.green), error: c(s.red), warning: c(s.yellow), warningShimmer: mix(s.fg, s.yellow, 0.35), chromeYellow: c(s.yellow),
40    fastMode: c(s.orange), fastModeShimmer: mix(s.fg, s.orange, 0.35),
41    diffAdded: mix(s.green, s.bg, 0.28), diffRemoved: mix(s.red, s.bg, 0.28), diffAddedDimmed: mix(s.green, s.bg, 0.14), diffRemovedDimmed: mix(s.red, s.bg, 0.14),
42    diffAddedWord: mix(s.green, s.bg, 0.55), diffRemovedWord: mix(s.red, s.bg, 0.55),
43    userMessageBackground: c(s.panel), userMessageBackgroundHover: mix(s.fg, s.panel, 0.06), composerSidebarBackground: c(s.panel),
44    bashMessageBackgroundColor: c(s.panel), memoryBackgroundColor: c(s.panel), selectionBg: mix(s.blue, s.bg, 0.35),
45    rate_limit_fill: c(s.blue), rate_limit_empty: c(s.subtle),
46    red_FOR_SUBAGENTS_ONLY: c(s.red), blue_FOR_SUBAGENTS_ONLY: c(s.blue), green_FOR_SUBAGENTS_ONLY: c(s.green), yellow_FOR_SUBAGENTS_ONLY: c(s.yellow),
47    purple_FOR_SUBAGENTS_ONLY: c(s.purple), orange_FOR_SUBAGENTS_ONLY: c(s.orange), pink_FOR_SUBAGENTS_ONLY: c(s.pink), cyan_FOR_SUBAGENTS_ONLY: c(s.cyan),
48  }
49  const rainbow = { red: s.red, orange: s.orange, yellow: s.yellow, green: s.green, blue: s.blue, indigo: s.purple, violet: s.pink }
50  for (const [k, v] of Object.entries(rainbow)) {
51    t[`rainbow_${k}`] = c(v)
52    t[`rainbow_${k}_shimmer`] = mix(s.fg, v, 0.35)
53  }
54  return t
55}
56
57/** The file Claude Code reads from ~/.claude/themes/<slug>.json. */
58export const themeFile = (s: Skin) => JSON.stringify({ name: s.name, base: 'dark', overrides: overridesFor(s) }, null, 2)
59export const themeRef = (s: Skin) => `custom:skin-${s.slug}`
60export const skinOf = (theme: unknown) => SKINS.find(s => themeRef(s) === theme) ?? null
61
types/index.d.ts 9 lines
1/** The theme last applied from the pane; writing it redraws the pane (a settings change alone doesn't). */
2export type Applied = string | null
3
4declare module 'claude-code' {
5  interface PluginState {
6    skins: { applied: Applied }
7  }
8}
9