SLOPSHOPPER

example-spinner

Example brand spinner for Claude Code (terminal and desktop app)

newspinnercommandprompttimer
★ 43v1.0.0NOASSERTIONupdated 2026-10-06cofoundy/brand-skills/skills/brand-mod/template
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · example-spinner
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /example-spinner ⎿ example-spinner: Example spinner off: back to Claude Code's own. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

<img src="./assets/logo.png" alt="brand skills" width="460" />

Brand Skills — Branding, Naming & Brand Identity for AI Agents

Turn an idea into a real brand — name, identity, voice, and a brand book — without leaving your AI agent. 16 open-source Agent Skills for founders, indie hackers, and agencies building AI-native — in Claude Code, Cursor, Windsurf, and 70+ agents.

npx skills add cofoundy/brand-skills

License: MIT Agent Skills Stars Install <!-- Re-add once skills.sh indexes the repo (post-install telemetry): skills.sh -->


New: your brand inside Claude Code

<img src="./skills/brand-mod/examples/cofoundy/mod/cofoundy-spinner/media/terminal.gif" alt="A branded Claude Code spinner in the terminal" width="760" /> <img src="./skills/brand-mod/examples/cofoundy/mod/cofoundy-spinner/media/desktop-light.gif" alt="The same brand in the Claude desktop app, light theme" width="49%" /> <img src="./skills/brand-mod/examples/cofoundy/mod/cofoundy-spinner/media/desktop-dark.gif" alt="The same brand in the Claude desktop app, dark theme" width="49%" />

brand-mod turns a finished brand into a Claude Code mod: while Claude works, the terminal and the desktop app show your colors, your words and your logo. It previews everything in HTML first, so nothing ships until you approve it. Great for screen recordings and demos. See the skill →


What you get

Describe your product in a sentence. Your agent runs the skills and hands back a complete, coherent brand:

  • A name that isn't AI slop — metaphor-driven, prior-art + domain checked, not a thesaurus mashup.
  • A full identity — positioning, architecture, visual brief, voice, and messaging that all tell the same story.
  • A brand book — guidelines ready to hand to a designer or a teammate.
  • Saved, not lost — every brand is a reusable brand.yaml package, not a chat you'll never find again.

One command, minutes — instead of a blank page, a freelancer brief, and three weeks.

Why it's different

Most "AI branding" hands you a logo with a tagline glued on. A logo isn't a brand. Brand Skills finds the story first, then carries it through every decision — so naming, voice, and messaging line up instead of fighting each other.

Built and dogfooded by Cofoundy on our own products. MIT-licensed — the whole pipeline is yours to read, fork, and ship.

Install

A — npx skills (recommended; works in 70+ agents)

Cross-agent install via the open Agent Skills CLI — Claude Code, Cursor, Codex, OpenCode, and more:

npx skills add cofoundy/brand-skills           # all skills
npx skills add cofoundy/brand-skills -s naming # one skill
npx skills add cofoundy/brand-skills -g        # install globally (user dir) instead of per-project

Installs to your agent's skills dir (e.g. .claude/skills/…, .cursor/…). Try one without installing: npx skills use cofoundy/brand-skills -s naming | claude.

B — Claude Code plugin

/plugin marketplace add cofoundy/brand-skills
/plugin install brand-skills@brand-skills

C — Local (before/without a remote)

npx skills add ./path/to/brand-skills          # or:
/plugin marketplace add /abs/path/to/brand-skills

Troubleshooting: if skills don't trigger after install, reload your agent (Claude Code: /reload-plugins or restart). Each skill is self-contained markdown — cat .claude/skills/naming/SKILL.md to confirm it landed.

The brand-genesis pipeline

        ┌─────────────┐
        │brand-context│  ← foundation. Every skill reads this first.
        └──────┬──────┘
               │
   naming ─→ brand-architecture ─→ brand-identity ─┐
   (metaphor   (standalone vs       (logo brief,    │
    engine)     sub-brand vs         palette, type) │
                house-of-brands)                    ▼
   brand-strategy · brand-positioning · target-audience · competitor-branding
                                                    │
   brand-voice · brand-messaging · brand-story ─────┤
                                                    ▼
                                          brand-guidelines  (the brand book)
                                          brand-audit · rebranding
                                          brand-mod  (your brand inside Claude Code)

Skills

SkillWhat it does
brand-initScaffolds a structured brand package (brand.yaml + folder) and registers it in a portfolio. The persistence layer every other skill reads/writes.
namingMetaphor-driven brand & product naming. 30–50 candidates → filtered → prior-art + availability checked → vetted finalists. Avoids AI slop.
brand-contextFoundation. Captures the brand DNA every other skill reads first.
brand-strategyBrand heart, archetype, values — the full strategy report.
brand-architectureStandalone vs sub-brand vs branded-house vs house-of-brands. Naming systems for product families.
brand-positioningCompetitive map, positioning territory + statement, proof points.
target-audienceICP, personas, psychographics, audience language.
competitor-brandingHow competitors brand themselves → gaps + differentiation.
brand-identityVisual identity brief — logo direction, color, type, imagery (to brief a designer).
brand-voiceVerbal identity — tone, voice qualities, vocabulary, writing rules.
brand-messagingMessaging hierarchy — taglines, value prop, key messages.
brand-storyOrigin story + founder narrative (long / short / one-liner).
brand-guidelinesThe brand book — logo usage, color, type, voice, application rules.
brand-auditBrand health across 6 dimensions.
rebrandingAudit → reposition → relaunch an existing brand.
brand-modYour brand inside Claude Code: a branded spinner for the terminal and desktop app, previewed in HTML before it ships. Claude Code only.

Scope: v0 is brand genesis — creating a brand. It does not do go-to-market (ads, channels, growth). That's intentional; a brand should exist before it's marketed. brand-mod is the one application skill: it carries a finished brand into the tool your team already works in.

Brand packages & registry (the persistence layer)

Unlike chat-only brand tools, Brand Skills writes a brand package — a versioned, queryable folder:

brand/
  brand.yaml          # manifest: name, one-liner, status, archetype, which artifacts exist
  context.md  naming.md  strategy.md  identity.md  voice.md  guidelines.md  …
  assets/

Managing several brands? A brands/registry.yaml indexes them so any agent can answer "what brands do we have / where / what's the one-liner" in one query. The registry is a SSOT file, not agent memory — git-tracked and shareable. Full spec: references/brand-package-spec.md.

Spanish / LATAM localization

First-class Spanish / LATAM support — Spanish phonosemantics, cultural-connotation screening across major markets, and regional conventions — alongside English. See references/localization-es-latam.md.

Credits

Built on two excellent MIT-licensed projects, absorbed and extended here (see NOTICE):

License

MIT © Cofoundy SAC. Upstream copyrights preserved in NOTICE.


<sub>Keywords: brand skills · branding · brand strategy · brand naming · brand identity · brand voice · brand positioning · brand guidelines · rebranding · agent skills · Claude Code · AI branding · brand book generator.</sub>

Source 3 files
hooks/register.tsx 122 lines
1// Branded spinner for Claude Code, generated by the brand-mod skill (cofoundy/brand-skills).
2// While Claude works: a turning 2×2 square of cubes + the word for the turn's phase, with one
3// brand-color glint crossing both; at the end of the turn, «<word> for 12s». In the desktop app,
4// the SVG logo with a soft sweep. /<brand>-spinner on|off turns it off, remembered across sessions.
5import type { Register } from 'claude-code'
6
7import { BRAND, DESKTOP_WORD, LOGO_PNG, LOGO_SVG } from './brand'
8import type { Cell } from './spinner'
9import { doneWord, elapsed, FRAME_MS, glintPos, hasGraphics, shimmerAt, spinnerWord, square } from './spinner'
10
11const MUTED = '#8A97A3'
12
13let on = true
14let graphics = false
15let startedAt = 0
16let seed = 0
17let timer: { cancel: () => void } | undefined
18
19const stop = () => {
20  timer?.cancel()
21  timer = undefined
22}
23
24function cells(Text: any, list: Cell[], prefix: string) {
25  return list.map((c, i) => (
26    <Text key={`${prefix}${i}`} color={c.color} backgroundColor={c.bg}>{c.ch}</Text>
27  ))
28}
29
30export const register: Register = hook => {
31  hook('session.start', async ($, e, next) => {
32    const started = await next(e)
33    try {
34      on = (await $.store.get('spinner')) !== 'off'
35      graphics = hasGraphics({
36        TMUX: await $.env.get('TMUX'),
37        STY: await $.env.get('STY'),
38        HERDR_ENV: await $.env.get('HERDR_ENV'),
39        ZELLIJ: await $.env.get('ZELLIJ'),
40        TERM_PROGRAM: await $.env.get('TERM_PROGRAM'),
41        KITTY_WINDOW_ID: await $.env.get('KITTY_WINDOW_ID'),
42      })
43      if (e.isInteractive) await $.command.register({ name: BRAND.command, description: `${BRAND.name} spinner: on | off` })
44    } catch {
45      // no store or no commands: the spinner stays on
46    }
47    return started
48  })
49
50  hook('prompt.submit', async ($, e, next) => {
51    if (on) {
52      try {
53        startedAt = await $.clock.now()
54        seed = Math.floor(Math.random() * 1000)
55        stop()
56        timer = $.clock.every(FRAME_MS, () => $.ui.invalidate('ui.render'))
57      } catch {
58        stop()
59      }
60    }
61    return next(e)
62  })
63
64  hook('turn.complete', async ($, e, next) => {
65    stop()
66    return next(e)
67  })
68
69  hook('session.end', async ($, e, next) => {
70    stop()
71    return next(e)
72  })
73
74  hook('command.run', { command: BRAND.command }, async ($, e) => {
75    const arg = e.args.trim().toLowerCase()
76    on = arg === 'on' ? true : arg === 'off' ? false : !on
77    await $.store.set('spinner', on ? 'on' : 'off').catch(() => undefined)
78    if (!on) stop()
79    return { text: on ? `${BRAND.name} spinner on: you will see it from your next turn.` : `${BRAND.name} spinner off: back to Claude Code's own.` }
80  })
81
82  hook('ui.render', { component: 'Spinner' }, async ($, e, next) => {
83    if (!on || (e.surface !== 'terminal' && e.surface !== 'desktop')) return next(e)
84    const now = await $.clock.now()
85    const frame = Math.floor(now / FRAME_MS)
86    const word = e.props.message ?? spinnerWord(e.props.mode, seed)
87    const tail = `… ${startedAt ? elapsed(now - startedAt) : ''}`
88    const { Box, Image, Svg, Text } = $.ui.resolve(e) as any
89
90    // Desktop app: the SVG logo as an image (an interactive frame is rebuilt every frame and
91    // flickers) and the word in one color (the light glint tones are unreadable on a light theme).
92    if (e.surface === 'desktop') {
93      return (
94        <Box>
95          {LOGO_SVG ? <Svg key="logo" source={LOGO_SVG} alt={BRAND.name} width={16} height={16} /> : null}
96          <Text> </Text>
97          <Text color={DESKTOP_WORD} bold>{word}</Text>
98          <Text color={MUTED}>{tail}</Text>
99        </Box>
100      )
101    }
102
103    const pos = glintPos([...word].length, frame)
104    const logo = graphics && LOGO_PNG
105    return (
106      <Box>
107        {cells(Text, square(now, pos), 'i')}
108        <Text> </Text>
109        {cells(Text, shimmerAt(word, pos - 3), 'w')}
110        <Text color={MUTED}>{tail}</Text>
111        {logo ? <Box flexGrow={1} /> : null}
112        {logo ? <Image key="logo" source={{ png: LOGO_PNG }} columns={2} rows={1} alt=" " /> : null}
113      </Box>
114    )
115  })
116
117  hook('ui.render', { component: 'TurnDuration' }, ($, e, next) => {
118    if (!on) return next(e)
119    return next({ ...e, props: { ...e.props, word: doneWord(e.props.durationMs) } })
120  })
121}
122
hooks/brand.ts 27 lines
1// GENERATED by skills/brand-mod/scripts/brand-mod.py from mod.json: do not edit by hand.
2// In the template it holds sample values so the tests run.
3export const BRAND = { name: 'Example', slug: 'example', url: 'example.com', command: 'example-spinner' }
4
5// Brand scale, shadow to light: [0..2] darks, [3] primary, [4..6] lights, [7] white.
6export const STOPS = ['#062033', '#0A3554', '#145A86', '#2185C7', '#4DA0D5', '#90C2E3', '#D3E7F4', '#FFFFFF']
7
8// Word color in the desktop app: one color, readable on light and dark themes.
9export const DESKTOP_WORD = '#2185C7'
10
11// One word per turn phase (Spinner.props.mode).
12export const WORDS: Record<string, string[]> = {
13  thinking: ['Thinking'],
14  'tool-use': ['Working'],
15  'tool-input': ['Preparing'],
16  responding: ['Answering'],
17  requesting: ['Loading'],
18}
19
20// End of turn: the engine draws «<word> for 12s».
21export const DONE = ['Done']
22
23// Logo for the desktop app (SVG with an SMIL sweep) and for the terminal (64 px PNG, base64, light
24// version for dark backgrounds). Empty: no logo is drawn.
25export const LOGO_SVG = ''
26export const LOGO_PNG = ''
27
hooks/spinner.ts 56 lines
1// Pure spinner logic (no `$`): rendering lives in register.tsx.
2// Icon: a square 2×2 grid of half blocks (▀ ▄), three cubes and a hole turning clockwise; each
3// character carries two cubes (top = color, bottom = bg). One glint crosses the square diagonally
4// and carries on through the word. Why this and not something else: the brand-mod skill's method.md.
5import { DONE, STOPS, WORDS } from './brand'
6
7export type Cell = { ch: string; color: string; bg?: string }
8
9export const FRAME_MS = 100 // the engine redraws the spinner at most 10 times a second
10export const HOLE_MS = 150
11const BASE = 3
12const tone = (i: number): string => STOPS[Math.max(0, Math.min(STOPS.length - 1, i))] ?? '#888888'
13const lit = (distance: number) => tone(Math.max(BASE, STOPS.length - 1 - Math.round(distance * 1.4)))
14
15// The glint crosses the square (columns -3 and -2) and carries on through the word (column 0 on;
16// -1 is the space), 1.5 columns per frame, then rests for 10 columns.
17export function glintPos(wordLength: number, frame: number): number {
18  return (Math.floor(frame * 1.5) % (wordLength + 3 + 10)) - 5
19}
20
21// Hole, clockwise: 0 top-left → 1 top-right → 2 bottom-right → 3 bottom-left.
22export function square(now: number, pos: number): Cell[] {
23  const hole = Math.floor(now / HOLE_MS) % 4
24  const cube = (x: number, y: number) => lit(Math.abs(x - 3 + 0.5 * y - pos))
25  const column = (x: number, up: number, down: number): Cell => {
26    if (hole === up) return { ch: '▄', color: cube(x, 1) }
27    if (hole === down) return { ch: '▀', color: cube(x, 0) }
28    return { ch: '▀', color: cube(x, 0), bg: cube(x, 1) }
29  }
30  return [column(0, 0, 3), column(1, 1, 2)]
31}
32
33export function shimmerAt(text: string, pos: number): Cell[] {
34  return [...text].map((ch, i) => ({ ch, color: lit(Math.abs(i - pos)) }))
35}
36
37export function spinnerWord(mode: string, seed: number): string {
38  const pool = WORDS[mode] ?? WORDS.responding ?? ['Working']
39  return pool[seed % pool.length] ?? 'Working'
40}
41
42export function doneWord(durationMs: number): string {
43  return DONE[durationMs % DONE.length] ?? 'Done'
44}
45
46export function elapsed(ms: number): string {
47  const s = Math.max(0, Math.floor(ms / 1000))
48  return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`
49}
50
51// Kitty graphics: Ghostty or kitty run directly; tmux, screen, herdr and zellij don't pass them through.
52export function hasGraphics(env: Record<string, string | undefined>): boolean {
53  if (env.TMUX || env.STY || env.HERDR_ENV || env.ZELLIJ) return false
54  return env.TERM_PROGRAM === 'ghostty' || Boolean(env.KITTY_WINDOW_ID)
55}
56