Nyan Cat flies with its rainbow above the prompt while Claude works. /nyan changes its size or turns it off.

Nyan Cat for Claude Code: while Claude is working, a pixel-art Nyan Cat flies with its rainbow above the prompt.

In Claude Code:
/plugin marketplace add besanek/claude-nyan-cat
/plugin install nyan-cat@claude-nyan-cat
Needs Claude Code 2.1.289 or newer (plugins with function hooks are early access).
Third-party marketplaces don't auto-update by default. To get new versions automatically, open /plugin → Marketplaces → claude-nyan-cat and turn on auto-update. Otherwise update by hand:
claude plugin update nyan-cat@claude-nyan-cat
Then run /reload-plugins or start a new session.
| Command | What it does |
|---|---|
/nyan big | big cat, 9 terminal rows (default) |
/nyan small | small cat, 4 rows |
/nyan off | turns the cat off |
/nyan on | turns it back on at the last size |
/nyan | toggles on/off |
The setting is kept across sessions. The band opens a row at a time, the cat flies in from the left when Claude starts and off to the right when it finishes, then the band closes again, so the transcript never jumps.
/nyan small takes less room:

The animation is drawn in the terminal with half-block characters (2 pixels per row), in true color. The desktop app and VS Code show a single line of text instead.
The cat is drawn with half-block characters (▀) in true color, so it looks best in a terminal that draws block elements itself and supports 24-bit color: Ghostty, kitty, iTerm2, WezTerm, GNOME Terminal and Ptyxis all do.
1.0 and open a new terminal tab.1.0.claude --plugin-dir ./plugins/nyan-cat # run Claude Code with the mod loaded from disk
claude plugin validate ./plugins/nyan-cat
claude plugin test ./plugins/nyan-cat
Nyan Cat was created by Chris Torres (nyan.cat). This is an unofficial fan mod, not affiliated with him or with Anthropic.
MIT
hooks/register.tsx 373 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { NyanSize } from '../types'
5
6// Two pixel rows per cell: '▀' paints the top pixel as foreground, the bottom as background.
7const HALF_BLOCK = 0x2580
8// The widest Raster a surface takes; the band spans the window up to this.
9const MAX_COLUMNS = 512
10// Each sprite's stars cover this many columns; wider bands repeat them.
11const STAR_TILE = 72
12const FRAME_MS = 90
13// Frames the fly-in and the fly-off take, whatever the band's width (about 1.2 s each).
14const FLIGHT_FRAMES = 14
15// The tail reaches this far left of the crust; the cat is gone once it clears the right edge.
16const TAIL_REACH = 5
17const KEY = 'nyan'
18const COMMAND = 'nyan'
19
20const SKY = 0x0b2a5c
21const FUR = 0x9a9a9a
22const STAR = 0xffffff
23const STAR_GLOW = 0x5f78a8
24const RAINBOW = [0xff1a1a, 0xff9900, 0xffee00, 0x33ee11, 0x0099ff, 0x6633ff]
25
26const PALETTE: Record<string, number> = {
27 K: 0x111111, // outline, pupils, mouth
28 T: 0xffcc99, // pop-tart crust
29 P: 0xff99ff, // frosting
30 D: 0xff3399, // sprinkles
31 G: FUR,
32 W: 0xffffff, // eye glint
33 R: 0xff8fab, // cheeks
34}
35
36const size = atom({ plugin: 'nyan-cat', key: 'size' } as const, 'big')
37const isOn = atom({ plugin: 'nyan-cat', key: 'isOn' } as const, true)
38
39type Layer = { x: number; y: number; lines: string[] }
40
41type Sprite = {
42 pixelRows: number
43 width: number
44 bandHeight: number
45 body: Layer[]
46 legs: Layer[]
47 tails: [number, number][][]
48 stars: [number, number][]
49}
50
51const BIG_BODY: Layer = {
52 x: 0,
53 y: 0,
54 lines: [
55 '..KKKKKKKKKKKKKK..',
56 '.KTTTTTTTTTTTTTTK.',
57 'KTTTPPPPPPPPPPTTTK',
58 'KTPPPPPPDPPDPPPPTK',
59 'KTPPDPPPPPPPPPPPTK',
60 'KTPPPPPPPPPPPDPPTK',
61 'KTPPPPPDPPPPPPPPTK',
62 'KTPPPPPPPPPPPPPPTK',
63 'KTPDPPPPPPDPPPPPTK',
64 'KTPPPPPDPPPPPPPPTK',
65 'KTPPPPPPPPPPPDPPTK',
66 'KTPPDPPPPPPPPPPPTK',
67 'KTTTPPPPPPPPPPTTTK',
68 '.KTTTTTTTTTTTTTTK.',
69 '..KKKKKKKKKKKKKK..',
70 ],
71}
72
73// Pointed ears, eyes with a glint, a nose between them, wide smile and blush at the edges.
74const BIG_HEAD: Layer = {
75 x: 9,
76 y: 3,
77 lines: [
78 '.KK........KK.',
79 '.KGK......KGK.',
80 '.KGGK....KGGK.',
81 '.KGGGKKKKGGGK.',
82 'KGGGGGGGGGGGGK',
83 'KGGGWKGGGWKGGK',
84 'KGGGKKGKGKKGGK',
85 'KRRGGGGGGGGRRK',
86 'KRRGKGGKGGKRRK',
87 'KGGGGKKKKKGGGK',
88 '.KGGGGGGGGGGK.',
89 '..KKKKKKKKKK..',
90 ],
91}
92
93// Two poses, stepped each frame: back paws under the crust, front paws under the head.
94const BIG_LEGS: Layer[] = [
95 { x: 0, y: 15, lines: ['.KGK.KGK...KGK....KGK.', '.KK..KK....KK.....KK..'] },
96 { x: 0, y: 15, lines: ['KGK.KGK...KGK....KGK..', 'KK..KK....KK.....KK...'] },
97]
98
99// Tail pixels left of the crust, as offsets from the cat's corner; it wags between two poses.
100const BIG_TAILS: [number, number][][] = [
101 [[-1, 7], [-2, 7], [-3, 6], [-4, 6], [-5, 5], [-1, 8], [-2, 8], [-3, 7], [-4, 7], [-5, 6]],
102 [[-1, 8], [-2, 8], [-3, 9], [-4, 9], [-5, 10], [-1, 9], [-2, 9], [-3, 10], [-4, 10], [-5, 11]],
103]
104
105const BIG_STARS: [number, number][] = [
106 [5, 1],
107 [17, 15],
108 [29, 5],
109 [41, 13],
110 [53, 2],
111 [64, 10],
112 [11, 8],
113 [47, 16],
114 [59, 6],
115]
116
117const BIG: Sprite = {
118 pixelRows: 18,
119 width: 23,
120 bandHeight: 2,
121 body: [BIG_BODY, BIG_HEAD],
122 legs: BIG_LEGS,
123 tails: BIG_TAILS,
124 stars: BIG_STARS,
125}
126
127// The first, four-row cat: no outline, one pixel per rainbow band.
128const SMALL: Sprite = {
129 pixelRows: 8,
130 width: 13,
131 bandHeight: 1,
132 body: [
133 {
134 x: 0,
135 y: 0,
136 lines: [
137 'TTTTTTTTT....',
138 'TPDPPPPPTG..G',
139 'TPPPPDPPGGGGG',
140 'TPPDPPPGGKGKG',
141 'TPPPPPPGRGGGR',
142 'TTTTTTTTGGGG.',
143 ],
144 },
145 ],
146 legs: [
147 { x: 0, y: 6, lines: ['.G.G...G.G...'] },
148 { x: 0, y: 6, lines: ['G.G...G.G....'] },
149 ],
150 tails: [
151 [[-1, 3], [-2, 3], [-3, 2], [-4, 2]],
152 [[-1, 4], [-2, 4], [-3, 5], [-4, 5]],
153 ],
154 stars: [[5, 0], [17, 7], [29, 2], [41, 6], [53, 1], [64, 5], [11, 4], [47, 3]],
155}
156
157const SPRITES: Record<NyanSize, Sprite> = { big: BIG, small: SMALL }
158
159// Where the cat sits while flying: centred in the band.
160function catHome(columns: number, sprite: Sprite): number {
161 return Math.max(0, Math.floor((columns - sprite.width) / 2))
162}
163
164// Cell rows of the sprite's full picture.
165function fullRows(sprite: Sprite): number {
166 return sprite.pixelRows / 2
167}
168
169// `visibleRows` crops the picture to its middle rows while the band opens and closes.
170function frame(tick: number, columns: number, sprite: Sprite, offset: number, visibleRows: number): string {
171 const { pixelRows, bandHeight } = sprite
172 const pixels = new Uint32Array(columns * pixelRows).fill(SKY)
173 const put = (x: number, y: number, color: number) => {
174 if (x >= 0 && x < columns && y >= 0 && y < pixelRows) pixels[y * columns + x] = color
175 }
176
177 // Stars drift at half the frame rate and now and then glint with a faint cross.
178 for (let tile = 0; tile * STAR_TILE < columns; tile++) {
179 for (const [tx, sy] of sprite.stars) {
180 const sx = tx + tile * STAR_TILE
181 if (sx >= columns) continue
182 const x = (((sx - Math.floor(tick / 2)) % columns) + columns) % columns
183 put(x, sy, STAR)
184 if ((Math.floor(tick / 2) + sx) % 10 === 0) {
185 put(x - 1, sy, STAR_GLOW)
186 put(x + 1, sy, STAR_GLOW)
187 put(x, sy - 1, STAR_GLOW)
188 put(x, sy + 1, STAR_GLOW)
189 }
190 }
191 }
192
193 const catX = catHome(columns, sprite) + offset
194 const bob = tick % 4 < 2 ? 0 : 1
195
196 for (let x = 0; x < catX; x++) {
197 const wave = Math.floor((x + tick) / 4) % 2
198 RAINBOW.forEach((color, i) => {
199 for (let dy = 0; dy < bandHeight; dy++) put(x, 1 + i * bandHeight + dy + wave, color)
200 })
201 }
202
203 const tail = sprite.tails[tick % 2] ?? []
204 for (const [dx, dy] of tail) put(catX + dx, dy + bob, FUR)
205
206 const layers = [sprite.legs[tick % 2], ...sprite.body]
207 for (const layer of layers) {
208 if (layer === undefined) continue
209 layer.lines.forEach((line, dy) => {
210 for (let dx = 0; dx < line.length; dx++) {
211 const color = PALETTE[line.charAt(dx)]
212 if (color !== undefined) put(catX + layer.x + dx, layer.y + dy + bob, color)
213 }
214 })
215 }
216
217 const top = Math.floor((fullRows(sprite) - visibleRows) / 2)
218 const cells = new Uint32Array(columns * visibleRows * 3)
219 for (let row = 0; row < visibleRows; row++) {
220 const pixelRow = (top + row) * 2
221 for (let x = 0; x < columns; x++) {
222 const at = (row * columns + x) * 3
223 cells[at] = HALF_BLOCK
224 cells[at + 1] = pixels[pixelRow * columns + x] ?? SKY
225 cells[at + 2] = pixels[(pixelRow + 1) * columns + x] ?? SKY
226 }
227 }
228
229 return new Uint8Array(cells.buffer).toBase64()
230}
231
232async function save($: EngineInterface, change: { size?: NyanSize; isOn?: boolean }) {
233 const { size: nextSize, isOn: nextIsOn } = change
234 if (nextSize !== undefined) {
235 await update($, size, () => nextSize)
236 await $.store.set('size', nextSize)
237 }
238 if (nextIsOn !== undefined) {
239 await update($, isOn, () => nextIsOn)
240 await $.store.set('isOn', nextIsOn)
241 }
242}
243
244const USAGE = 'Usage: /nyan big | small | off | on (no argument toggles on/off)'
245
246export const register: Register = on => {
247 let tick = 0
248 // `offset` is how far the cat is from its centred home: negative while it flies in
249 // from the left, positive while it flies off to the right after the turn. `rows` is
250 // how tall the band is drawn now: it opens a row per frame at the start and, once
251 // the cat is gone, closes a row per frame, so the transcript above never jumps.
252 let mounted: {
253 requestId: string
254 columns: number
255 sprite: Sprite
256 phase: 'arriving' | 'flying' | 'departing' | 'closing'
257 offset: number
258 rows: number
259 } | null = null
260
261 on('session.start', async ($, e, next) => {
262 const storedSize = await $.store.get('size')
263 const storedOn = await $.store.get('isOn')
264 if (storedSize === 'big' || storedSize === 'small') await update($, size, () => storedSize)
265 if (typeof storedOn === 'boolean') await update($, isOn, () => storedOn)
266
267 await $.command.register({
268 name: COMMAND,
269 description: 'Nyan Cat: change its size or turn it off',
270 argumentHint: 'big | small | off | on',
271 immediate: true,
272 })
273
274 $.clock.every(FRAME_MS, () => {
275 if (mounted === null) return
276 tick += 1
277 const home = catHome(mounted.columns, mounted.sprite)
278 const rowsBefore = mounted.rows
279 if (mounted.phase === 'arriving') {
280 mounted.offset = Math.min(0, mounted.offset + Math.ceil((home + mounted.sprite.width) / FLIGHT_FRAMES))
281 if (mounted.offset === 0) mounted.phase = 'flying'
282 } else if (mounted.phase === 'departing') {
283 mounted.offset += Math.ceil((mounted.columns - home + TAIL_REACH) / FLIGHT_FRAMES)
284 if (home + mounted.offset - TAIL_REACH >= mounted.columns) mounted.phase = 'closing'
285 }
286
287 if (mounted.phase === 'closing') {
288 mounted.rows -= 1
289 } else if (mounted.rows < fullRows(mounted.sprite)) {
290 mounted.rows += 1
291 }
292
293 if (mounted.rows <= 0) {
294 mounted = null
295 $.ui.invalidate('ui.render')
296 return
297 }
298 if (mounted.rows !== rowsBefore) {
299 // Another height is a new Raster, not a blit: draw the band again.
300 $.ui.invalidate('ui.render')
301 return
302 }
303 const { requestId, columns, sprite, offset, rows } = mounted
304 void $.ui.blit({ requestId, key: KEY, cells: frame(tick, columns, sprite, offset, rows) })
305 })
306
307 return next(e)
308 })
309
310 on('command.run', { command: COMMAND }, async ($, e) => {
311 const arg = e.args.trim().toLowerCase()
312 const wasOn = await read($, isOn)
313
314 if (arg === 'big' || arg === 'small') {
315 await save($, { size: arg, isOn: true })
316 return { text: arg === 'big' ? 'big cat (9 rows)' : 'small cat (4 rows)' }
317 }
318 if (arg === 'off' || (arg === '' && wasOn)) {
319 await save($, { isOn: false })
320 return { text: 'off, turn it back on with /nyan on' }
321 }
322 if (arg === 'on' || arg === '') {
323 await save($, { isOn: true })
324 return { text: `on (${await read($, size)} cat)` }
325 }
326
327 return { text: USAGE }
328 })
329
330 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
331 const sprite = SPRITES[await read($, size)]
332 const rows = sprite.pixelRows / 2
333 const canShow = (await read($, isOn)) && !e.props.hasSurvey && e.props.maxRows >= rows
334 const isDeparting = !e.props.isWorking && mounted !== null
335
336 if (!canShow || (!e.props.isWorking && !isDeparting)) {
337 mounted = null
338 return next(e)
339 }
340
341 if (e.surface !== 'terminal') {
342 mounted = null
343 if (!e.props.isWorking) return next(e)
344 const { Text } = $.ui.resolve(e)
345 return <Text color="magenta">🌈🌈🌈 nyan nyan nyan 🐱</Text>
346 }
347
348 const columns = Math.min(MAX_COLUMNS, e.props.bodyColumns)
349 // A turn starting (or one starting while the cat was leaving) brings it in from the left;
350 // a turn ending sends it off to the right; any other redraw keeps it where it is.
351 const isLeaving = mounted?.phase === 'departing' || mounted?.phase === 'closing'
352 const startsFlight = e.props.isWorking && (mounted === null || isLeaving)
353 const phase = startsFlight
354 ? 'arriving'
355 : isDeparting
356 ? mounted?.phase === 'closing' ? 'closing' : 'departing'
357 : (mounted?.phase ?? 'flying')
358 const offset = startsFlight ? -(catHome(columns, sprite) + sprite.width) : (mounted?.offset ?? 0)
359 const visibleRows = Math.min(rows, mounted?.rows ?? 1)
360 const { Raster } = $.ui.resolve(e)
361 mounted = { requestId: e.requestId, columns, sprite, phase, offset, rows: visibleRows }
362
363 return (
364 <Raster
365 key={KEY}
366 columns={columns}
367 rows={visibleRows}
368 cells={frame(tick, columns, sprite, offset, visibleRows)}
369 />
370 )
371 })
372}
373types/index.d.ts 8 lines1export type NyanSize = 'big' | 'small'
2
3declare module 'claude-code' {
4 interface PluginState {
5 'nyan-cat': { size: NyanSize; isOn: boolean }
6 }
7}
8