Clawd lives in a band above the prompt and plays 200 animations, every second one with full-width scenery

A Claude Code mod: Clawd moves into a band above the prompt and plays 200 animations with props, friends and full-width scenery.
<img width="2876" height="396" alt="Screenshot_2026-10-06_14-02-15" src="https://github.com/user-attachments/assets/ee3b8e36-8486-4358-9977-d4ed1484b6af" />
In Claude Code:
/plugin install clawd-stage --marketplace MarcBlattmann/clawd-stage
Answer y to add the marketplace, then pick a scope.
Requires Claude Code 2.1.291 or newer, in a terminal. Mods are early access in Claude Code and may change between releases.
| Command | |
|---|---|
/clawd | Hide or show the stage |
/clawd <id> | Play one animation now, e.g. /clawd surf-wave |
The stage takes 7 rows above the prompt and hides itself when the band has fewer.
200 files in anims/, the ids being the file names.
| Kind | Count | Examples |
|---|---|---|
| Full-width scenery | 50 | train-journey, city-lights, surf-wave, northern-lights, platformer-level |
| Everything else | 150 | balloon-pump-pop, western-duel, snowball-fight, dj-booth, zipline |
Scenery and non-scenery animations alternate. docs/ANIMATIONS.md describes how to write one.
Node.js 18 or newer.
| Task | Command |
|---|---|
| Run Claude Code with the mod from this folder | claude --plugin-dir . |
Rebuild hooks/engine.js after editing src/ or anims/ | node tools/build.js |
| Check every animation | node tools/sim.js check all |
| Preview one as text | node tools/sim.js preview anims/surf-wave.js --W 100 |
| Validate the plugin | claude plugin validate . |
| Run the tests | claude plugin test . |
hooks/engine.js is generated but committed, because an install runs the repository as it is.
| Path | |
|---|---|
hooks/register.tsx | The band, the frame timer and /clawd |
hooks/draw.ts | Turns a frame into Raster cells |
hooks/engine.js | Built from src/ and anims/ |
src/core.js | Animation registry, helpers and scheduler |
src/sprite.js | Clawd's sprite glyphs and the colour palette |
anims/ | One file per animation |
tools/ | build.js and sim.js |
tests/ | Cases for claude plugin test |
MIT. Clawd is Anthropic's mascot; this is an unofficial fan mod, not affiliated with Anthropic.
hooks/register.tsx 116 lines1// Clawd's stage as a band above the prompt: a 7-row Raster repainted with $.ui.blit on every
2// animation frame. The animations and the scheduler live in hooks/engine.js, which
3// tools/build.js builds from src/ and anims/.
4import { atom, read, update } from 'claude-code'
5import type { EngineInterface, Register } from 'claude-code'
6
7import { $cdH, $cdInt, $cdL, $cdMs, $cdNext, $cdPlay } from './engine.js'
8import type { CdFrame, CdState } from './engine.js'
9import { compose, encode, refused } from './draw'
10
11const isHidden = atom({ plugin: 'clawd-stage', key: 'isHidden' } as const, false)
12
13const REST: CdFrame = { pose: 'default', offset: 0 }
14const KEY = 'stage'
15
16// Short dances between animations.
17function moves(): Record<string, CdFrame[]> {
18 const P = (eyes: string, arms = 'down', feet = 'both') => ({ eyes, arms, feet })
19 const F = (pose: unknown, n: number, offset = 0): CdFrame[] => Array.from({ length: n }, () => ({ pose, offset }))
20 const R = (q: CdFrame[], n: number): CdFrame[] => Array.from({ length: n }, () => q).flat()
21 const pop: CdFrame[] = [{ pose: 'default', offset: 1, poof: 'dot' }, { pose: 'default', offset: 1, poof: 'wave' }]
22 return {
23 shuffle: [...R([...F(P('left', 'up', 'left'), 2), ...F(P('right', 'up', 'right'), 2)], 4), REST],
24 wave: [...R([...F(P('open', 'one-up'), 3), ...F(P('open', 'down'), 3)], 3), ...F(P('wink', 'one-up'), 5), REST],
25 headbang: [...R([...F(P('closed', 'up'), 2, 1), ...F(P('closed', 'up'), 2)], 5), REST],
26 roof: [...R([...F(P('open', 'up'), 2), ...F(P('open', 'down'), 2, 1)], 4), ...pop, REST],
27 disco: [...R([...F(P('right', 'one-up', 'right'), 3), ...F(P('left', 'down', 'left'), 3)], 3), REST],
28 jacks: [...R([...F(P('open', 'up'), 2), ...F(P('closed', 'down'), 2, 1)], 4), ...pop, REST],
29 }
30}
31
32// Module state: a reload starts the stage over.
33const st: CdState = { x: 0, seq: [], i: 0, hist: [], moves: moves() }
34let frame: CdFrame = REST
35let width = 0
36let site: string | undefined
37let hidden = false
38
39async function paint($: EngineInterface) {
40 if (!site || !width) return
41 const { deny } = await $.ui.blit({ requestId: site, key: KEY, cells: encode(compose(frame, width)), columns: width, rows: $cdH })
42 if (!deny) return
43 // A glyph the surface won't draw: remember it, draw `*` instead, and redraw the band.
44 const cell = /\b(?:cell|index)\D{0,12}(\d+)/i.exec(deny)
45 if (cell) {
46 const cp = compose(frame, width)[Number(cell[1]) * 3]
47 if (cp !== undefined && cp !== 0x20) refused.add(cp)
48 }
49 site = undefined
50 $.ui.invalidate('ui.render')
51}
52
53function tick($: EngineInterface) {
54 let delay = 250
55 if (!hidden && width > 0) {
56 if (st.i >= st.seq.length) {
57 st.seq = $cdNext(st, width) || []
58 st.i = 0
59 }
60 const f = st.seq[st.i++] ?? REST
61 if (f.x !== undefined) st.x = $cdInt(f.x, st.x)
62 frame = { ...f, x: st.x }
63 delay = $cdMs(f.ms)
64 void paint($)
65 }
66 $.clock.after(delay, () => tick($))
67}
68
69export const register: Register = (on) => {
70 on('session.start', async ($, e, next) => {
71 hidden = await read($, isHidden)
72 await $.command.register({
73 name: 'clawd',
74 description: 'Show or hide Clawd above the prompt, or play one animation',
75 argumentHint: '[animation]',
76 })
77 tick($)
78 return next(e)
79 })
80
81 on('command.run', { command: 'clawd' }, async ($, e) => {
82 const id = e.args.trim()
83 if (!id) {
84 hidden = !hidden
85 await update($, isHidden, () => hidden)
86 return { text: hidden ? 'Clawd hidden' : 'Clawd shown' }
87 }
88 const anim = $cdL.find((a) => a.id === id)
89 if (!anim) return { text: `No animation "${id}" (${$cdL.length} available)` }
90 if (hidden) {
91 hidden = false
92 await update($, isHidden, () => false)
93 }
94 st.seq = $cdPlay(st, Math.max(width, 80), anim)
95 st.i = 0
96 return { text: `Playing ${anim.meta.title ?? id}` }
97 })
98
99 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
100 if (e.surface !== 'terminal' || e.props.hasSurvey || e.props.maxRows < $cdH || (await read($, isHidden))) {
101 site = undefined
102 return next(e)
103 }
104 const { Box, Raster } = $.ui.resolve(e)
105 width = Math.max(20, Math.min(512, e.props.bodyColumns))
106 site = e.requestId
107 const below = await next(e)
108 return (
109 <Box flexDirection="column">
110 <Raster key={KEY} columns={width} rows={$cdH} cells={encode(compose(frame, width))} />
111 {below}
112 </Box>
113 )
114 })
115}
116hooks/draw.ts 131 lines1// Turns one stage frame into Raster cells, layered back to front: back props, back actors,
2// Clawd and his effects, front actors, front props.
3import { $cdCol, $cdG, $cdH, $cdInt, $cdPalette, $cdPose, $cdRuns, $cdSprite } from './engine.js'
4import type { CdActor, CdFrame } from './engine.js'
5
6const DEFAULT = 0x01000000
7const BLACK = 0x000000
8const BODY = $cdPalette.clawd_body ?? 0xd77757
9const DIM = $cdPalette.inactive ?? 0x999999
10const TEXT = $cdPalette.text ?? 0xffffff
11const STAR = 0x2a
12const ARMS_DOWN = { r1L: ' ▐', r1R: '', r2L: '▝▜', r2R: '█▀' }
13
14// Code points the surface refused once (learned from a blit's deny), drawn as `*` from then on.
15export const refused = new Set<number>()
16const WIDE = /\p{Emoji_Presentation}|\p{M}|\p{Cf}/u
17
18function glyph(ch: string): number {
19 const cp = ch.codePointAt(0) ?? 0x20
20 if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0)) return 0x20
21 if (cp > 0xffff || refused.has(cp) || WIDE.test(ch)) return STAR
22 return cp
23}
24
25function color(c: unknown, fallback: number): number {
26 const s = $cdCol(c)
27 if (s === undefined) return fallback
28 const named = $cdPalette[s]
29 if (named !== undefined) return named
30 const m = /^rgb\((\d+),(\d+),(\d+)\)$/.exec(s)
31 return m ? (Number(m[1]) << 16) | (Number(m[2]) << 8) | Number(m[3]) : fallback
32}
33
34export function compose(fr: CdFrame, W: number): Uint32Array {
35 const H = $cdH
36 const cells = new Uint32Array(W * H * 3)
37 for (let i = 0; i < W * H; i++) {
38 cells[i * 3] = 0x20
39 cells[i * 3 + 1] = DEFAULT
40 cells[i * 3 + 2] = DEFAULT
41 }
42 const put = (x: number, y: number, ch: string, fg: number, bg: number) => {
43 if (x < 0 || x >= W || y < 0 || y >= H) return
44 const k = (y * W + x) * 3
45 cells[k] = glyph(ch)
46 cells[k + 1] = fg
47 cells[k + 2] = bg
48 }
49 const props = (front: boolean) => {
50 for (const p of fr.props ?? []) {
51 if (!p || typeof p !== 'object' || (Number((p as { z?: unknown }).z) < 0) === front) continue
52 for (const run of $cdRuns(p, W, H)) {
53 const fg = color(run.c, TEXT)
54 const bg = color(run.bg, DEFAULT)
55 Array.from(run.t).forEach((ch, i) => put(run.x + i, run.y, ch, fg, bg))
56 }
57 }
58 }
59 const sprite = (a: CdActor) => {
60 if (a.hide) return
61 const ox = $cdInt(a.x, 0)
62 const oy = $cdG + $cdInt(a.offset, 0)
63 const base = color(a.color, BODY)
64 const pose = $cdPose(a.pose)
65 if (typeof pose === 'object' && 'facing' in pose) {
66 ($cdSprite.facing[pose.facing] ?? []).forEach((row, r) =>
67 Array.from(row.glyphs).forEach((ch, c) => put(ox + c, oy + r, ch, base, c >= row.from && c < row.to ? BLACK : DEFAULT)))
68 return
69 }
70 const p = typeof pose === 'string' ? $cdSprite.poses[pose] : pose
71 if (!p) return
72 const paint = typeof a.paint === 'function' ? a.paint : undefined
73 const at = (c: number, r: number) => {
74 if (!paint) return base
75 try { return color(paint(c, r), base) } catch { return base }
76 }
77 // Stock rule: a space takes the colour before it; eyes sit on black, a closed lid inverts.
78 const seg = (glyphs: string, col: number, row: number, on?: 'eyes' | 'lid') => {
79 let prev: number | undefined
80 Array.from(glyphs).forEach((ch, i) => {
81 const fg = ch === ' ' && prev !== undefined ? prev : at(col + i, row)
82 prev = fg
83 if (on === 'lid') put(ox + col + i, oy + row, ch, BLACK, fg)
84 else put(ox + col + i, oy + row, ch, fg, on === 'eyes' ? BLACK : DEFAULT)
85 })
86 }
87 const arms = $cdSprite.arms[p.arms] ?? $cdSprite.arms.down ?? ARMS_DOWN
88 seg(arms.r1L, 0, 0)
89 let col = 2
90 for (const span of $cdSprite.eyes[p.eyes] ?? $cdSprite.eyes.open ?? []) {
91 seg(span.glyphs, col, 0, span.lid ? 'lid' : 'eyes')
92 col += Array.from(span.glyphs).length
93 }
94 seg(arms.r1R, 8, 0)
95 seg(arms.r2L, 0, 1)
96 seg('█████', 2, 1, 'eyes')
97 seg(arms.r2R, 7, 1)
98 seg($cdSprite.feet[p.feet] ?? $cdSprite.feet.both ?? '', 0, 2)
99 }
100
101 props(false)
102 for (const a of fr.actors ?? []) if (a && typeof a === 'object' && !a.front) sprite(a)
103 if (!fr.hide) {
104 const x = $cdInt(fr.x, 0)
105 const o = $cdInt(fr.offset, 0)
106 sprite({ x, offset: o, pose: fr.pose, color: fr.color, paint: fr.paint })
107 const poof = fr.poof === 'dot' ? '·' : fr.poof === 'wave' ? '~' : undefined
108 if (poof && o > 0) [x, x + 8].forEach((px) => put(px, H - 1, poof, DIM, DEFAULT))
109 const shadow = fr.shadow === 'wide' ? { g: '▁▁▁', l: 3 } : fr.shadow === 'narrow' ? { g: '▁', l: 4 } : undefined
110 if (shadow) Array.from(shadow.g).forEach((ch, i) => put(x + shadow.l + i, H - 1, ch, DIM, DEFAULT))
111 }
112 for (const a of fr.actors ?? []) if (a && typeof a === 'object' && a.front) sprite(a)
113 props(true)
114 return cells
115}
116
117const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
118
119export function encode(cells: Uint32Array): string {
120 const bytes = new Uint8Array(cells.length * 4)
121 const view = new DataView(bytes.buffer)
122 cells.forEach((v, i) => view.setUint32(i * 4, v >>> 0, true))
123 let out = ''
124 for (let i = 0; i < bytes.length; i += 3) {
125 const a = bytes[i] ?? 0, b = bytes[i + 1], c = bytes[i + 2]
126 const n = (a << 16) | ((b ?? 0) << 8) | (c ?? 0)
127 out += B64.charAt((n >> 18) & 63) + B64.charAt((n >> 12) & 63) + (b === undefined ? '=' : B64.charAt((n >> 6) & 63)) + (c === undefined ? '=' : B64.charAt(n & 63))
128 }
129 return out
130}
131types/index.d.ts 8 lines1export type ClawdStageVisibility = boolean
2
3declare module 'claude-code' {
4 interface PluginState {
5 'clawd-stage': { isHidden: ClawdStageVisibility }
6 }
7}
8