Reskins the transcript's tool rows: an icon and a colour per tool, from a theme palette. /skin <name> or /skin off.

My Claude Code mods, as a plugin marketplace (alex-mods). One folder per mod under plugins/.
| Mod | What it does |
|---|---|
| savvy-progress | Ember progress bar above the prompt for a batch of subagents, and an Agents pane: crab mascot per agent, live status, per-agent progress bar, model, tokens, estimated cost, elapsed time. |
| file-tree | Files pane with the project tree: files Claude reads (blue), modifies (orange) or creates (purple) light up with a shimmer, and turn green once committed, in every repo they live in. |
| tool-skins | Reskins the transcript's tool rows: an icon and a palette colour per tool (dracula skin from kevinvn1709/vscode-dracula-color-theme), the row's result still drawn by Claude Code. |
| cache-tax | Prompt-cache state in the status line; stops a message that would re-read the whole conversation on a cold cache and shows what it costs; /keepwarm keeps the cache warm while you are away. |
claude plugin marketplace add E:/WS/claude-mods
claude plugin install savvy-progress@alex-mods --scope user
claude plugin install file-tree@alex-mods --scope user
claude plugin install cache-tax@alex-mods --scope user
claude plugin install tool-skins@alex-mods --scope user
Installed this way, Claude Code reads the plugin from this folder, so an edit here reaches a session with /reload-plugins (no reinstall, no version bump).
/plugin install savvy-progress --marketplace Fraktalis/claude-mods
Then update later with claude plugin update savvy-progress@alex-mods after pushing a new version.
claude plugin validate plugins/savvy-progress
claude plugin test plugins/savvy-progress
Bump version in the mod's .claude-plugin/plugin.json for each release that GitHub installs should pick up.
/savvy opens the Agents pane; /savvy-skin <type|*> <crab|developer|designer|researcher|architect|ghost> picks a mascot.progress tool (mcp__savvy-progress__progress); each subagent is asked to report done/total/step with it. The main loop can also call it, with title to name the batch.hooks/register.tsx, PRICES); edit it when prices change./tree opens the Files pane (it also opens by itself on the first file Claude edits or creates); /tree-clear forgets what was touched.git commit run by Claude re-checks git status in every repository holding a modified or new file; commits made in a terminal are picked up on the next prompt.node_modules, .git and the like are listed dimmed and stay closed. Touched only hides the rest. Files outside the session's folder are listed under Elsewhere.🔥 cache 42m while warm, 🧊 cache cold · next msg ≈ $2.40 once the TTL has passed since the main conversation's last request.hooks/register.ts (PRICES), same table as savvy-progress./cache shows the details; /keepwarm on|off sends a one-line ping ~1m30 before expiry, at most maxPings times in a row (never while Claude works or while you type). Each ping costs a cache read and a one-word reply.ttl (1h or 5m), minTokens, maxPings. The TTL is not detected: switch it to 5m in usage overage.ToolUse): icon + tool name in its palette colour, the call's argument (last 3 path segments, or the one-line command), … running, ✗ error, Interrupted. The result under the row (ToolResult: diffs, Bash output) is still Claude Code's own./skin lists skins, /skin <name> switches, /skin off hands the rows back; the choice is kept across sessions. dracula is on by default.dracula comes from themes/dracula-color-theme.json of github.com/kevinvn1709/vscode-dracula-color-theme, a Darcula-style palette despite its name: keyword #CC7832 → Bash, function #FFC66D → Edit, string #6A8759 → Write, number #6897BB → Read, type #4EC9B0 → Grep/Glob, property #9876AA → Agent, info #6796e6 → web tools, debug #b267e6 → MCP tools, on #212122.THEMES (hooks/register.tsx). The name claude-skins is reserved for Anthropic, hence tool-skins.hooks/register.tsx 148 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4const skin = atom({ plugin: 'tool-skins', key: 'skin' } as const, null)
5
6// ---------- themes ----------
7
8export type Look = { icon: string; color: string; bold?: boolean }
9
10export type Theme = {
11 /** Where the palette comes from, shown by /skin. */
12 source: string
13 /** Row background; absent leaves the transcript's own. */
14 background?: string
15 text: string
16 muted: string
17 running: string
18 error: string
19 tools: Record<string, Look>
20 /** MCP tools (`mcp__server__tool`). */
21 mcp: Look
22 fallback: Look
23}
24
25/**
26 * "dracula" from github.com/kevinvn1709/vscode-dracula-color-theme
27 * (themes/dracula-color-theme.json): despite the name, a Darcula-style palette.
28 * Token colours mapped to tools by role: keywords -> Bash, functions -> Edit,
29 * strings -> Write, numbers -> Read, types -> search, properties -> Agent.
30 */
31const DRACULA: Theme = {
32 source: 'github.com/kevinvn1709/vscode-dracula-color-theme',
33 background: '#212122', // editor.background
34 text: '#A9B7C6', // variables / plain text
35 muted: '#808080', // comment
36 running: '#cd9731', // token.warn-token
37 error: '#f44747', // invalid / token.error-token
38 tools: {
39 Bash: { icon: '❯', color: '#CC7832', bold: true }, // keyword (bold)
40 Read: { icon: '▤', color: '#6897BB' }, // constant.numeric
41 Edit: { icon: '✎', color: '#FFC66D' }, // entity.name.function
42 MultiEdit: { icon: '✎', color: '#FFC66D' },
43 NotebookEdit: { icon: '✎', color: '#FFC66D' },
44 Write: { icon: '✚', color: '#6A8759' }, // string
45 Grep: { icon: '⌕', color: '#4EC9B0' }, // entity.name.type
46 Glob: { icon: '✱', color: '#4EC9B0' },
47 WebFetch: { icon: '⇣', color: '#6796e6' }, // token.info-token
48 WebSearch: { icon: '⌕', color: '#6796e6' },
49 Agent: { icon: '◆', color: '#9876AA' }, // variable / property
50 TodoWrite: { icon: '☑', color: '#808080' },
51 TaskCreate: { icon: '☐', color: '#808080' },
52 TaskUpdate: { icon: '☑', color: '#808080' },
53 Skill: { icon: '✦', color: '#d7ba7d' }, // entity.name.tag.css
54 ToolSearch: { icon: '⌕', color: '#BABABA' }, // entity.other.attribute-name
55 },
56 mcp: { icon: '⧉', color: '#b267e6' }, // token.debug-token
57 fallback: { icon: '•', color: '#d4d4d4' }, // editor.foreground
58}
59
60export const THEMES: Record<string, Theme> = { dracula: DRACULA }
61const DEFAULT_SKIN = 'dracula'
62
63export const lookOf = (theme: Theme, tool: string): Look => theme.tools[tool] ?? (tool.startsWith('mcp__') ? theme.mcp : theme.fallback)
64
65/** `mcp__obsidian__read_note` -> `obsidian · read_note`; built-ins stay as they are. */
66export const toolLabel = (tool: string) => {
67 const m = /^mcp__(.+?)__(.+)$/.exec(tool)
68 return m ? `${m[1]} · ${m[2]}` : tool
69}
70
71/** The one argument worth showing for a call, shortened. */
72export const argOf = (input: unknown, max = 90) => {
73 const i = (input ?? {}) as Record<string, unknown>
74 const pick = [i.file_path, i.notebook_path, i.command, i.pattern, i.url, i.query, i.description, i.skill, i.subject, i.prompt].find(
75 v => typeof v === 'string' && v.trim() !== '',
76 ) as string | undefined
77 if (!pick) return ''
78 const one = pick.replace(/\s+/g, ' ').trim()
79 // a path: keep the last three segments
80 const isPath = (typeof i.file_path === 'string' || typeof i.notebook_path === 'string') && pick === (i.file_path ?? i.notebook_path)
81 const shown = isPath ? one.replace(/\\/g, '/').split('/').slice(-3).join('/') : one
82 return shown.length > max ? `${shown.slice(0, max - 1)}…` : shown
83}
84
85// ---------- hooks ----------
86
87export const register: Register = on => {
88 on('session.start', async ($, e, next) => {
89 await $.command.register({
90 name: 'skin',
91 description: 'Reskin tool rows: /skin <name>, /skin off, /skin to list',
92 argumentHint: `<${[...Object.keys(THEMES), 'off'].join('|')}>`,
93 })
94 // never chosen yet: start on the default skin; an explicit /skin off is kept (null)
95 const saved = (await $.store.get('skin')) as string | null | undefined
96 const start = saved === undefined ? DEFAULT_SKIN : saved === null || THEMES[saved] ? saved : DEFAULT_SKIN
97 await update($, skin, () => start)
98 return next(e)
99 })
100
101 on('command.run', { command: 'skin' }, async ($, e) => {
102 const arg = e.args.trim().toLowerCase()
103 const names = Object.keys(THEMES)
104 if (!arg) {
105 const cur = await read($, skin)
106 return { text: `Skin: ${cur ?? 'off'}. Available: ${names.join(', ')}, off.` }
107 }
108 if (arg !== 'off' && !THEMES[arg]) return { text: `No skin "${arg}". Available: ${names.join(', ')}, off.` }
109 const next = arg === 'off' ? null : arg
110 await update($, skin, () => next)
111 await $.store.set('skin', next)
112 return { text: next ? `Skin "${next}" on (palette from ${THEMES[next]!.source}).` : 'Skins off: the engine draws its own rows.' }
113 })
114
115 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
116 const name = await read($, skin)
117 const theme = name ? THEMES[name] : undefined
118 if (!theme) return next(e)
119
120 const { Box, Text } = $.ui.resolve(e)
121 const p = e.props
122 const look = lookOf(theme, p.tool)
123 const arg = argOf(p.input)
124 const status = p.isInterrupted ? (
125 <Text color={theme.muted}> Interrupted</Text>
126 ) : p.isErrored ? (
127 <Text color={theme.error}> ✗</Text>
128 ) : p.isRunning ? (
129 <Text color={theme.running}> …</Text>
130 ) : null
131
132 return (
133 <Box flexDirection="row" backgroundColor={theme.background} paddingX={1}>
134 <Text color={look.color} bold={look.bold}>
135 {look.icon} {toolLabel(p.tool)}
136 </Text>
137 {arg ? (
138 <Text color={theme.text} wrap="truncate-end">
139 {' '}
140 {arg}
141 </Text>
142 ) : null}
143 {status}
144 </Box>
145 )
146 })
147}
148types/index.d.ts 12 lines1/** A skin's name, a key of THEMES in hooks/register.tsx. */
2export type SkinName = string
3
4declare module 'claude-code' {
5 interface PluginState {
6 'tool-skins': {
7 /** The active skin; null draws the engine's own rows. */
8 skin: SkinName | null
9 }
10 }
11}
12