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

<img src="./assets/logo.png" alt="brand skills" width="460" />
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
<!-- Re-add once skills.sh indexes the repo (post-install telemetry):
-->
<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 →
Describe your product in a sentence. Your agent runs the skills and hands back a complete, coherent brand:
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.
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.
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.
/plugin marketplace add cofoundy/brand-skills
/plugin install brand-skills@brand-skills
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.
┌─────────────┐
│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)
| Skill | What it does |
|---|---|
| brand-init | Scaffolds a structured brand package (brand.yaml + folder) and registers it in a portfolio. The persistence layer every other skill reads/writes. |
| naming | Metaphor-driven brand & product naming. 30–50 candidates → filtered → prior-art + availability checked → vetted finalists. Avoids AI slop. |
| brand-context | Foundation. Captures the brand DNA every other skill reads first. |
| brand-strategy | Brand heart, archetype, values — the full strategy report. |
| brand-architecture | Standalone vs sub-brand vs branded-house vs house-of-brands. Naming systems for product families. |
| brand-positioning | Competitive map, positioning territory + statement, proof points. |
| target-audience | ICP, personas, psychographics, audience language. |
| competitor-branding | How competitors brand themselves → gaps + differentiation. |
| brand-identity | Visual identity brief — logo direction, color, type, imagery (to brief a designer). |
| brand-voice | Verbal identity — tone, voice qualities, vocabulary, writing rules. |
| brand-messaging | Messaging hierarchy — taglines, value prop, key messages. |
| brand-story | Origin story + founder narrative (long / short / one-liner). |
| brand-guidelines | The brand book — logo usage, color, type, voice, application rules. |
| brand-audit | Brand health across 6 dimensions. |
| rebranding | Audit → reposition → relaunch an existing brand. |
| brand-mod | Your 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-modis the one application skill: it carries a finished brand into the tool your team already works in.
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.
First-class Spanish / LATAM support — Spanish phonosemantics, cultural-connotation screening across major markets, and regional conventions — alongside English. See references/localization-es-latam.md.
Built on two excellent MIT-licensed projects, absorbed and extended here (see NOTICE):
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>
hooks/register.tsx 122 lines1// 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}
122hooks/brand.ts 27 lines1// 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 = ''
27hooks/spinner.ts 56 lines1// 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