SLOPSHOPPER

coral-skin

Coral, ink and cream reskin of Claude Code with risk-coded tool rows. Toggle with /skin.

newspinnerrowsguardcommandtimer
★ 11v0.1.0NOASSERTIONupdated 2026-10-02promptadvisers/claude-mods-starter-kit/plugins/coral-skin
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · coral-skin
▌ ❯ fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⟨Claude Code's own drawing⟩ EDIT ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ⟨Claude Code's own drawing⟩ ✳ Worked for 42s ────────────────── › /skin ✳ CORAL SKIN skin on · risk colors on. Try /skin on, /skin off, /skin risk. ✳ Inking… 0s · 9 tools · 3 edits ! 1 risky ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Spinner
✳ Inking… 0s · 9 tools · 3 edits ! 1 risky
Turn line
✳ Worked for 42s ──────────────────
Your message
▌ ❯ fix the failing auth test and add an audit log call
Claude's reply
⟨Claude Code's own drawing⟩
Tool row
⟨Claude Code's own drawing⟩ EDIT
Command output
✳ CORAL SKIN skin on · risk colors on. Try /skin on, /skin off, /skin risk.
README

Coral Skin

Changes supported Claude Code message and tool rows to a coral, ink and cream style. Colored chips help you spot reads, edits and commands.

See the complete guide and creation prompt.

Test from the kit root:

claude plugin validate plugins/coral-skin
claude plugin test plugins/coral-skin
Source 2 files
hooks/coral-skin.mjs 363 lines
1// Coral Skin: coral, ink and cream reskin with risk-coded tool rows.
2// Visual only. Uses no filesystem, network, model, or permission APIs.
3// Toggle with /skin, /skin on, /skin off, /skin risk.
4
5import { update } from "claude-code";
6
7const C = {
8  coral: "#e8603c",
9  coralDim: "#a8472e",
10  ink: "#141413",
11  card: "#1f1c19",
12  cream: "#f6efe3",
13  paper: "#fbf6ee",
14  muted: "#b8ad9c",
15  sage: "#9cc5a1",
16  sageBg: "#1e2a21",
17  amber: "#f0a33a",
18  red: "#d93b3b",
19};
20
21const prefsRef = { plugin: "coral-skin", key: "prefs" };
22const turnRef = { plugin: "coral-skin", key: "turn" };
23const STORE_KEY = "prefs";
24const DEFAULT_PREFS = { enabled: true, risk: true };
25const EMPTY_TURN = { active: false, startedAt: 0, now: 0, frame: 0, reads: 0, edits: 0, bash: 0, danger: 0, other: 0 };
26
27const READ_TOOLS = new Set(["Read", "Grep", "Glob", "LS", "WebFetch", "WebSearch", "NotebookRead", "TodoWrite", "ToolSearch"]);
28const EDIT_TOOLS = new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
29const SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
30const DESTRUCTIVE = [
31  /\brm\s+(-[a-zA-Z]*[rRf][a-zA-Z]*\s+)+/,
32  /\brm\s+--(recursive|force)\b/,
33  /\bgit\s+reset\s+--hard\b/,
34  /\bgit\s+push\b[^|;&]*\s(--force\b|--force-with-lease\b|-f\b)/,
35  /\bgit\s+clean\s+-[a-zA-Z]*f/,
36  /\bgit\s+checkout\s+(--\s+)?\.\s*$/,
37  /\bgit\s+branch\s+-D\b/,
38  /\bdrop\s+(table|database|schema)\b/i,
39  /\btruncate\s+table\b/i,
40  /\bdelete\s+from\s+\w+\s*(;|$)/i,
41  /\bmkfs(\.\w+)?\b/,
42  /\bdd\s+if=/,
43  /\bchmod\s+-R\s+777\b/,
44  /:\(\)\s*\{\s*:\|:&\s*\};:/,
45  />\s*\/dev\/(sd|disk|nvme)/,
46];
47
48const CHIPS = {
49  read: { label: " READ ", fg: C.sage, bg: C.sageBg, bold: false, rule: C.sage },
50  edit: { label: " EDIT ", fg: C.ink, bg: C.coral, bold: true, rule: C.coral },
51  bash: { label: " BASH ", fg: C.ink, bg: C.amber, bold: true, rule: C.amber },
52  danger: { label: " ! RISK ", fg: C.paper, bg: C.red, bold: true, rule: C.red },
53  other: { label: " TOOL ", fg: C.cream, bg: "#2a2725", bold: false, rule: C.muted },
54};
55const RANK = { other: 0, read: 1, bash: 2, edit: 3, danger: 4 };
56
57const VERBS = {
58  thinking: "Inking",
59  requesting: "Sketching",
60  responding: "Writing",
61  "tool-use": "Working",
62  "tool-input": "Drafting",
63};
64const PULSE = ["✳", "✴", "✶", "✴"];
65
66let ticker = null; // a timer handle; a reload drops it with the old environment
67
68export function register(on) {
69  on("session.start", async ($, e, next) => {
70    const result = await next(e);
71    const saved = await $.store.get(STORE_KEY);
72    await $.state.set(prefsRef, normalizePrefs(saved));
73    await $.state.set(turnRef, EMPTY_TURN);
74    await $.command.register({
75      name: "skin",
76      description: "Coral skin: show status, or turn it on, off, or toggle risk colors",
77      argumentHint: "[on|off|risk]",
78      immediate: true,
79    });
80    return result;
81  });
82
83  on("command.run", { command: "skin" }, async ($, e) => {
84    const { value: current } = await $.state.get(prefsRef);
85    const prefs = normalizePrefs(current);
86    const arg = (e.args || "").trim().toLowerCase();
87    let nextPrefs = prefs;
88    if (arg === "on") nextPrefs = { ...prefs, enabled: true };
89    else if (arg === "off") nextPrefs = { ...prefs, enabled: false };
90    else if (arg === "risk") nextPrefs = { ...prefs, risk: !prefs.risk };
91    else if (arg === "risk on") nextPrefs = { ...prefs, risk: true };
92    else if (arg === "risk off") nextPrefs = { ...prefs, risk: false };
93    else if (arg !== "" && arg !== "status") {
94      return { text: `Unknown option "${arg}". Use /skin, /skin on, /skin off or /skin risk.` };
95    }
96    if (nextPrefs !== prefs) {
97      await $.state.set(prefsRef, nextPrefs);
98      await $.store.set(STORE_KEY, nextPrefs);
99      $.ui.invalidate("ui.render");
100    }
101    return { text: statusLine(nextPrefs) };
102  });
103
104  // Turn bookkeeping for the spinner and the hint tail.
105  on("turn.start", async ($, e, next) => {
106    const result = await next(e);
107    const now = await $.clock.now();
108    await $.state.set(turnRef, { ...EMPTY_TURN, active: true, startedAt: now, now });
109    if (ticker) ticker.cancel();
110    ticker = $.clock.every(500, () => {
111      void (async () => {
112        const at = await $.clock.now();
113        await update($, turnRef, (t) => ({ ...(t || EMPTY_TURN), now: at, frame: ((t && t.frame) || 0) + 1 }));
114      })();
115    });
116    return result;
117  });
118
119  on("turn.complete", async ($, e, next) => {
120    if (ticker) {
121      ticker.cancel();
122      ticker = null;
123    }
124    const now = await $.clock.now();
125    await update($, turnRef, (t) => ({ ...(t || EMPTY_TURN), active: false, now }));
126    return next(e);
127  });
128
129  on("tool.call", async ($, e, next) => {
130    const kind = riskOf(e.tool, e);
131    const field = kind === "danger" ? "danger" : kind === "edit" ? "edits" : kind === "bash" ? "bash" : kind === "read" ? "reads" : "other";
132    await update($, turnRef, (t) => {
133      const base = t || EMPTY_TURN;
134      return { ...base, [field]: (base[field] || 0) + 1 };
135    });
136    return next(e);
137  });
138
139  // Spinner: coral pulse, mode verb, elapsed time and tool count.
140  on("ui.render", { component: "Spinner" }, async ($, e, next) => {
141    const { value: p } = await $.state.get(prefsRef);
142    if (!normalizePrefs(p).enabled) return next(e);
143    const { value: t } = await $.state.get(turnRef);
144    const turn = t || EMPTY_TURN;
145    const tools = toolCount(turn);
146    if (e.surface !== "terminal") {
147      const suffix = tools > 0 ? ` ✳ ${tools} tool${tools === 1 ? "" : "s"}` : " ✳";
148      return next({ ...e, props: { ...e.props, suffix } });
149    }
150    const { Box, Text } = $.ui.resolve(e);
151    const verb = e.props.message || VERBS[e.props.mode] || e.props.word || "Working";
152    const seconds = turn.startedAt ? Math.max(0, Math.floor((turn.now - turn.startedAt) / 1000)) : 0;
153    return spinnerTree(Box, Text, { verb, frame: turn.frame, seconds, tools, danger: turn.danger, edits: turn.edits });
154  });
155
156  // Tool rows: a risk chip in front of the engine's own row.
157  on("ui.render", { component: "ToolUse" }, async ($, e, next) => {
158    const { value: p } = await $.state.get(prefsRef);
159    const prefs = normalizePrefs(p);
160    if (!prefs.enabled || !prefs.risk) return next(e);
161    const kind = riskOf(e.props.tool, e.props.input);
162    if (kind === "other") return next(e); // neutral tools (questions, agents, MCP) keep the stock row
163    const original = await next(e);
164    if (!original) return original;
165    const { Box, Text } = $.ui.resolve(e);
166    return chipRow(Box, Text, kind, original, null);
167  });
168
169  on("ui.render", { component: "ToolGroup" }, async ($, e, next) => {
170    const { value: p } = await $.state.get(prefsRef);
171    const prefs = normalizePrefs(p);
172    if (!prefs.enabled || !prefs.risk) return next(e);
173    const original = await next(e);
174    if (!original || e.props.isExpanded) return original;
175    const { Box, Text } = $.ui.resolve(e);
176    let top = "other";
177    for (const call of e.props.calls) {
178      const kind = riskOf(call.tool, call.input);
179      if (RANK[kind] > RANK[top]) top = kind;
180    }
181    if (top === "other") return original;
182    return chipRow(Box, Text, top, original, e.props.calls.length);
183  });
184
185  // The person's prompt: a card with a coral rule.
186  on("ui.render", { component: "UserMessage" }, async ($, e, next) => {
187    const { value: p } = await $.state.get(prefsRef);
188    if (!normalizePrefs(p).enabled) return next(e);
189    const { Box, Text } = $.ui.resolve(e);
190    if (e.props.origin.kind !== "composer") {
191      const original = await next(e);
192      if (!original) return original;
193      return Box({ flexDirection: "row", children: [Text({ color: C.coralDim, children: "▎" }), original] });
194    }
195    const paper = e.surface !== "terminal";
196    const bg = paper ? C.paper : C.card;
197    const fg = paper ? C.ink : C.cream;
198    return Box({
199      flexDirection: "row",
200      backgroundColor: bg,
201      paddingRight: 1,
202      children: [
203        Text({ color: C.coral, backgroundColor: bg, bold: true, children: "▌ ❯ " }),
204        Text({ color: fg, backgroundColor: bg, wrap: "wrap", children: e.props.text }),
205      ],
206    });
207  });
208
209  // Assistant replies: the engine's markdown, behind a coral rule.
210  on("ui.render", { component: "AssistantMessage" }, async ($, e, next) => {
211    const { value: p } = await $.state.get(prefsRef);
212    const original = await next(e);
213    if (!normalizePrefs(p).enabled || !original) return original;
214    const { Box } = $.ui.resolve(e);
215    return Box({
216      flexDirection: "row",
217      children: [
218        Box({ width: 1, flexShrink: 0, backgroundColor: e.props.isFirstOfReply ? C.coral : C.coralDim }),
219        Box({ flexDirection: "column", flexGrow: 1, flexShrink: 1, paddingLeft: 1, children: [original] }),
220      ],
221    });
222  });
223
224  // Turn footer (terminal only): coral glyph, cream words, a coral rule.
225  on("ui.render", { component: "TurnDuration" }, async ($, e, next) => {
226    const { value: p } = await $.state.get(prefsRef);
227    if (!normalizePrefs(p).enabled) return next(e);
228    const { Box, Text } = $.ui.resolve(e);
229    return Box({
230      flexDirection: "row",
231      children: [
232        Text({ color: C.coral, bold: true, children: "✳ " }),
233        Text({ color: C.cream, children: `${e.props.word} for ${duration(e.props.durationMs)}` }),
234        Text({ color: C.coralDim, wrap: "truncate", children: " ──────────────────" }),
235      ],
236    });
237  });
238
239  // Hint line: keep the engine's line and its pills, add a risk tally.
240  on("ui.render", { component: "PromptHint" }, async ($, e, next) => {
241    const { value: p } = await $.state.get(prefsRef);
242    if (!normalizePrefs(p).enabled) return next(e);
243    const { value: t } = await $.state.get(turnRef);
244    return next({ ...e, props: { ...e.props, tail: hintTail(t || EMPTY_TURN) } });
245  });
246
247  on("ui.render", { component: "SessionMode" }, async ($, e, next) => {
248    const { value: p } = await $.state.get(prefsRef);
249    if (!normalizePrefs(p).enabled) return next(e);
250    return next({ ...e, props: { ...e.props, modes: [...e.props.modes, "coral skin"] } });
251  });
252
253  // Questions: the engine's dialog, untouched, inside a coral frame.
254  on("ui.render", { component: "AskUserQuestion" }, async ($, e, next) => {
255    const { value: p } = await $.state.get(prefsRef);
256    const original = await next(e);
257    if (!normalizePrefs(p).enabled || !original) return original;
258    const { Box, Text } = $.ui.resolve(e);
259    // No side border: the dialog sizes its rules to the full width and would wrap inside one.
260    return Box({
261      flexDirection: "column",
262      children: [
263        Box({
264          flexDirection: "row",
265          children: [
266            Text({ color: C.ink, backgroundColor: C.coral, bold: true, children: " ✳ CLAUDE IS ASKING " }),
267            Text({ color: C.coral, wrap: "truncate", children: " " + "─".repeat(60) }),
268          ],
269        }),
270        original,
271      ],
272    });
273  });
274
275  // The /skin command's own output row.
276  on("ui.render", { component: "CommandOutput", props: { command: "skin" } }, async ($, e, next) => {
277    if (e.props.isErrored) return next(e);
278    const { Box, Text } = $.ui.resolve(e);
279    return Box({
280      flexDirection: "row",
281      children: [
282        Text({ color: C.ink, backgroundColor: C.coral, bold: true, children: " ✳ CORAL SKIN " }),
283        Text({ color: e.surface === "terminal" ? C.cream : undefined, children: ` ${e.props.text.replace(/^coral-skin:\s*/, "")}` }),
284      ],
285    });
286  });
287}
288
289function normalizePrefs(v) {
290  const o = v && typeof v === "object" ? v : {};
291  return {
292    enabled: typeof o.enabled === "boolean" ? o.enabled : DEFAULT_PREFS.enabled,
293    risk: typeof o.risk === "boolean" ? o.risk : DEFAULT_PREFS.risk,
294  };
295}
296
297function statusLine(prefs) {
298  const skin = prefs.enabled ? "on" : "off (stock rendering)";
299  const risk = prefs.risk ? "on" : "off";
300  return `skin ${skin} · risk colors ${risk}. Try /skin on, /skin off, /skin risk.`;
301}
302
303export function riskOf(tool, input) {
304  if (EDIT_TOOLS.has(tool)) return "edit";
305  if (READ_TOOLS.has(tool)) return "read";
306  if (SHELL_TOOLS.has(tool)) {
307    const command = input && typeof input.command === "string" ? input.command : "";
308    return DESTRUCTIVE.some((re) => re.test(command)) ? "danger" : "bash";
309  }
310  return "other";
311}
312
313function chipRow(Box, Text, kind, original, count) {
314  const chip = CHIPS[kind];
315  const label = count && count > 1 ? `${chip.label.trimEnd()} ×${count} ` : chip.label;
316  // The engine's row opens with a blank margin line; the chip sits beside the header under it.
317  return Box({
318    flexDirection: "row",
319    children: [
320      Box({
321        flexShrink: 0,
322        marginTop: 1,
323        children: [Text({ color: chip.fg, backgroundColor: chip.bg, bold: chip.bold, wrap: "truncate", children: label.padEnd(10) })],
324      }),
325      Box({ flexDirection: "column", flexGrow: 1, flexShrink: 1, children: [original] }),
326    ],
327  });
328}
329
330function spinnerTree(Box, Text, s) {
331  const glyph = PULSE[s.frame % PULSE.length];
332  const bright = s.frame % 2 === 0;
333  const parts = [
334    Text({ color: bright ? C.coral : C.coralDim, bold: true, children: `${glyph} ` }),
335    Text({ color: C.coral, bold: true, children: `${s.verb}…` }),
336    Text({ color: C.cream, children: ` ${duration(s.seconds * 1000)}` }),
337  ];
338  if (s.tools > 0) parts.push(Text({ color: C.muted, children: ` · ${s.tools} tool${s.tools === 1 ? "" : "s"}` }));
339  if (s.edits > 0) parts.push(Text({ color: C.coral, children: ` · ${s.edits} edit${s.edits === 1 ? "" : "s"}` }));
340  if (s.danger > 0) parts.push(Text({ color: C.paper, backgroundColor: C.red, bold: true, children: ` ! ${s.danger} risky ` }));
341  return Box({ flexDirection: "row", children: parts });
342}
343
344function toolCount(t) {
345  return (t.reads || 0) + (t.edits || 0) + (t.bash || 0) + (t.danger || 0) + (t.other || 0);
346}
347
348function hintTail(t) {
349  const bits = [];
350  if (t.reads) bits.push(`${t.reads} read`);
351  if (t.edits) bits.push(`${t.edits} edit`);
352  if (t.bash) bits.push(`${t.bash} bash`);
353  if (t.danger) bits.push(`${t.danger} risky`);
354  return bits.length ? `✳ coral · ${bits.join(" · ")}` : "✳ coral";
355}
356
357function duration(ms) {
358  const total = Math.max(0, Math.round(ms / 1000));
359  const m = Math.floor(total / 60);
360  const s = total % 60;
361  return m > 0 ? `${m}m ${s}s` : `${s}s`;
362}
363
types/index.d.ts 19 lines
1export type CoralSkinPrefs = { enabled: boolean; risk: boolean };
2export type CoralSkinTurn = {
3  active: boolean;
4  startedAt: number;
5  now: number;
6  frame: number;
7  reads: number;
8  edits: number;
9  bash: number;
10  danger: number;
11  other: number;
12};
13
14declare module "claude-code" {
15  interface PluginState {
16    "coral-skin": { prefs: CoralSkinPrefs; turn: CoralSkinTurn };
17  }
18}
19