A pixel slime above the prompt that grows as you work with Claude

Mods for Claude Code, in the terminal and in the desktop app's Code tab: little games and toys that live above the prompt, written as plugins of function hooks.
![]()
An idle game in the band above the prompt. Racks of servers fill up with data, daemon bots walk over to collect it, and you spend it on more racks, more daemons, overclocking and better hardware (Raspberry Pi, Tower, Blade, GPU rig).
Your real work is the weather:
npm test, vitest, pytest and the like) pay 30 seconds of productionThe farm keeps earning while you are away, at half rate for up to 8 hours. With several sessions open, one runs the farm and the others mirror it and send it their events.
Commands: /farm opens the shop (keys 1 to 4 buy), /farm stats, /farm hide, /farm show.
![]()
A pixel slime that hatches from an egg and grows into a teen and then a slime king as you work. It glances around while Claude works, cheers when tests pass, frowns when a tool fails and falls asleep when you leave.
Commands: /pet, /pet pat, /pet name <name>, /pet hide, /pet show.
Both mods draw the band above the prompt, so install one at a time.
/plugin marketplace add pierregoutheraud/claude-mods
/plugin install server-farm@pierre-mods
They watch turns and tool calls (to see whether a call failed or ran tests, never to change it), add their slash command, draw the band above the prompt and keep their save in their own plugin store. No network, no file access, no processes. claude plugin validate plugins/<name> lists every hook and call a mod makes.
Load a mod straight from this folder; it hot-reloads when you save:
claude --plugin-dir plugins/server-farm
To load it in every session, the desktop app included, add its absolute path to CLAUDE_CODE_PLUGIN_DIRS in the env block of your Claude Code settings.json. Do not run a dev copy and an installed copy of the same mod at once.
Run the checks before committing (CI runs the same script):
./scripts/check.sh
It validates the marketplace and each plugin, runs each plugin's tests with claude plugin test, and type-checks a plugin once Claude Code has loaded it, which is when its types land in .claude-plugin/types/ (gitignored).
The function hooks API is early access and can change between Claude Code releases. These mods were built and tested on Claude Code 2.1.287, and CI runs against the latest release on every push.
Bump version in the plugin's .claude-plugin/plugin.json, commit, tag and push. Users get it with /plugin marketplace update pierre-mods.
.claude-plugin/marketplace.json the marketplace: its name and the plugins it lists
plugins/<name>/
.claude-plugin/plugin.json the plugin's manifest
hooks/hooks.json names the hooks module
hooks/register.tsx the hooks: events in, drawing out
hooks/*.ts pure logic and drawing, tested directly
types/index.d.ts the plugin's $.state contract
tests/ claude plugin test runs these
scripts/check.sh validate, test, type-checkhooks/register.tsx 264 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Pet, PetMood } from '../types'
5import { COLUMNS, ROWS, drawPet, kaomoji, nextStageAt, PALETTES, stageLabel, stageOf, stageStart } from './sprites'
6
7const TICK_MS = 500
8const FLASH_MS = 5000
9const SLEEP_AFTER_MS = 10 * 60 * 1000
10const TESTS_XP = 3
11const TEST_COMMAND = /\b(vitest|jest|pytest|mocha|playwright test|go test|cargo test|(npm|pnpm|yarn|bun) (run )?test)\b/
12
13const pet = atom({ plugin: 'pixel-pet', key: 'pet' } as const, null)
14const frame = atom({ plugin: 'pixel-pet', key: 'frame' } as const, 0)
15const flash = atom({ plugin: 'pixel-pet', key: 'flash' } as const, null)
16const isAsleep = atom({ plugin: 'pixel-pet', key: 'isAsleep' } as const, false)
17const isHidden = atom({ plugin: 'pixel-pet', key: 'isHidden' } as const, false)
18
19const isPet = (value: unknown): value is Pet =>
20 typeof value === 'object' &&
21 value !== null &&
22 typeof (value as Pet).name === 'string' &&
23 typeof (value as Pet).xp === 'number' &&
24 typeof (value as Pet).bornAt === 'number'
25
26const xpBar = (xp: number): string => {
27 const stage = stageOf(xp)
28 const next = nextStageAt(stage)
29 if (next === undefined) return `${xp} xp, fully grown`
30
31 const from = stageStart(stage)
32 const filled = Math.round(((xp - from) / (next - from)) * 10)
33
34 return `${'█'.repeat(filled)}${'░'.repeat(10 - filled)} ${xp}/${next} xp`
35}
36
37const moodLine = (name: string, mood: PetMood, frame: number, isEgg: boolean): string => {
38 const dots = '.'.repeat(1 + (frame % 3))
39 switch (mood) {
40 case 'working':
41 return isEgg ? `${name} wobbles${dots}` : `${name} is reading over Claude's shoulder${dots}`
42 case 'sleeping':
43 return `${name} is asleep ${'z'.repeat(1 + (frame % 3))}`
44 case 'idle':
45 return isEgg ? `${name} is an egg. It hatches at 3 xp.` : `${name} is waiting for your next prompt`
46 default:
47 return name
48 }
49}
50
51// When the person or Claude last did something; a hot reload starts it over.
52let lastActiveAt = 0
53
54async function flashMood($: EngineInterface, mood: 'happy' | 'sad', text: string) {
55 const until = (await $.clock.now()) + FLASH_MS
56 await update($, flash, () => ({ mood, text, until }))
57}
58
59async function wake($: EngineInterface) {
60 lastActiveAt = await $.clock.now()
61 if (await read($, isAsleep)) await update($, isAsleep, () => false)
62}
63
64async function pat($: EngineInterface) {
65 await wake($)
66 await flashMood($, 'happy', 'purrs happily')
67}
68
69async function savePet($: EngineInterface, next: Pet) {
70 await update($, pet, () => next)
71 await $.store.set('pet', next)
72}
73
74async function gainXp($: EngineInterface, amount: number) {
75 const before = await read($, pet)
76 if (before === null) return
77
78 const after = { ...before, xp: before.xp + amount }
79 await savePet($, after)
80
81 const grown = stageOf(after.xp)
82 if (grown !== stageOf(before.xp)) {
83 $.ui.toast(`${after.name} evolved into ${stageLabel(grown)}!`)
84 await flashMood($, 'happy', `evolved into ${stageLabel(grown)}!`)
85 }
86}
87
88async function setHidden($: EngineInterface, hidden: boolean) {
89 await update($, isHidden, () => hidden)
90 await $.store.set('isHidden', hidden)
91}
92
93async function tick($: EngineInterface) {
94 const now = await $.clock.now()
95 await update($, frame, n => n + 1)
96
97 const shown = await read($, flash)
98 if (shown !== null && now >= shown.until) await update($, flash, () => null)
99
100 if (now - lastActiveAt >= SLEEP_AFTER_MS && !(await read($, isAsleep))) {
101 await update($, isAsleep, () => true)
102 }
103}
104
105export const register: Register = on => {
106 on('session.start', async ($, e, next) => {
107 const now = await $.clock.now()
108 lastActiveAt = now
109
110 const stored = await $.store.get('pet')
111 const loaded = isPet(stored) ? stored : { name: 'Byte', xp: 0, bornAt: now }
112 await savePet($, loaded)
113
114 const hidden = (await $.store.get('isHidden')) === true
115 await update($, isHidden, () => hidden)
116
117 await $.command.register({
118 name: 'pet',
119 description: 'Check on your pixel pet (/pet, /pet pat, /pet name <name>, /pet hide, /pet show)',
120 })
121
122 // Headless runs draw nothing; a timer there would only keep the process busy.
123 if (e.isInteractive) {
124 $.clock.every(TICK_MS, () => {
125 void tick($).catch(() => undefined)
126 })
127 }
128
129 return next(e)
130 })
131
132 on('turn.start', async ($, e, next) => {
133 await wake($)
134
135 return next(e)
136 })
137
138 on('tool.call', async ($, e, next) => {
139 const ran = await next(e)
140 if (ran.deny !== undefined) return ran
141
142 if (ran.isError === true) {
143 await flashMood($, 'sad', `winces: ${String(e.tool)} failed`)
144 } else if (e.tool === 'Bash' && TEST_COMMAND.test(e.command)) {
145 await flashMood($, 'happy', `loved that: tests passed! (+${TESTS_XP} xp)`)
146 await gainXp($, TESTS_XP)
147 }
148
149 return ran
150 })
151
152 on('turn.complete', async ($, e, next) => {
153 if (e.agentId === undefined) {
154 await wake($)
155 if (e.reason === 'answer') await gainXp($, 1)
156 if (e.reason === 'error') await flashMood($, 'sad', 'got scared by an API error')
157 }
158
159 return next(e)
160 })
161
162 on('command.run', { command: 'pet' }, async ($, e) => {
163 const current = await read($, pet)
164 if (current === null) return { text: 'Your pet has not arrived yet.' }
165
166 const [verb = '', ...rest] = e.args.trim().split(/\s+/)
167 switch (verb) {
168 case '': {
169 await setHidden($, false)
170 const stage = stageOf(current.xp)
171 const next = nextStageAt(stage)
172 const days = Math.floor(((await $.clock.now()) - current.bornAt) / 86_400_000)
173 const growth = next === undefined ? 'It is fully grown.' : `It grows up at ${next} xp.`
174
175 return { text: `${current.name} is ${stageLabel(stage)} with ${current.xp} xp, ${days} days old. ${growth}` }
176 }
177 case 'pat':
178 await pat($)
179
180 return { text: `You pat ${current.name}.` }
181 case 'name': {
182 const name = rest.join(' ').slice(0, 20)
183 if (name === '') return { text: 'Usage: /pet name <name>' }
184 await savePet($, { ...current, name })
185
186 return { text: `Your pet is now called ${name}.` }
187 }
188 case 'hide':
189 await setHidden($, true)
190
191 return { text: `${current.name} is napping out of sight. /pet show brings it back.` }
192 case 'show':
193 await setHidden($, false)
194
195 return { text: `${current.name} is back above the prompt.` }
196 default:
197 return { text: 'Usage: /pet [pat | name <name> | hide | show]' }
198 }
199 })
200
201 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
202 const current = await read($, pet)
203 if (e.props.hasSurvey || current === null || (await read($, isHidden))) return next(e)
204
205 const beat = await read($, frame)
206 const shown = await read($, flash)
207 const asleep = await read($, isAsleep)
208 const mood: PetMood = shown?.mood ?? (e.props.isWorking ? 'working' : asleep ? 'sleeping' : 'idle')
209 const stage = stageOf(current.xp)
210 const line = shown === null ? moodLine(current.name, mood, beat, stage === 'egg') : `${current.name} ${shown.text}`
211 const color = PALETTES[stage].b
212
213 const { Box, Button, Text } = $.ui.resolve(e)
214
215 if (e.props.bodyColumns < 44) {
216 return (
217 <Text wrap="truncate">
218 {kaomoji(stage, mood, beat)} {line}
219 </Text>
220 )
221 }
222
223 const info = (
224 <Box flexDirection="column" flexShrink={1}>
225 <Box flexDirection="row" gap={1}>
226 <Text bold color={color}>
227 {current.name}
228 </Text>
229 <Text dimColor>{stageLabel(stage)}</Text>
230 </Box>
231 <Text italic wrap="truncate">
232 {line}
233 </Text>
234 <Text dimColor>
235 {xpBar(current.xp)}
236 </Text>
237 <Box flexDirection="row">
238 <Button key="pat" label="pat" onPress={() => pat($)} />
239 </Box>
240 </Box>
241 )
242
243 if (e.surface === 'terminal') {
244 const { Raster } = $.ui.resolve(e)
245
246 return (
247 <Box flexDirection="row" gap={2}>
248 <Raster key="sprite" columns={COLUMNS} rows={ROWS} cells={drawPet(stage, mood, beat)} />
249 {info}
250 </Box>
251 )
252 }
253
254 return (
255 <Box flexDirection="row" gap={2}>
256 <Text bold color={color}>
257 {kaomoji(stage, mood, beat)}
258 </Text>
259 {info}
260 </Box>
261 )
262 })
263}
264hooks/sprites.ts 202 lines1import type { PetMood, PetStage } from '../types'
2
3// The sprite is 14x12 pixels, drawn two pixels per terminal cell with half blocks.
4export const COLUMNS = 14
5export const ROWS = 6
6
7export const STAGES: readonly { stage: PetStage; from: number; label: string }[] = [
8 { stage: 'egg', from: 0, label: 'an egg' },
9 { stage: 'baby', from: 3, label: 'a baby slime' },
10 { stage: 'teen', from: 25, label: 'a teen slime' },
11 { stage: 'adult', from: 100, label: 'a slime king' },
12]
13
14export const stageOf = (xp: number): PetStage =>
15 STAGES.reduce<PetStage>((found, step) => (xp >= step.from ? step.stage : found), 'egg')
16
17export const stageLabel = (stage: PetStage): string =>
18 STAGES.find(step => step.stage === stage)?.label ?? stage
19
20export const stageStart = (stage: PetStage): number =>
21 STAGES.find(step => step.stage === stage)?.from ?? 0
22
23// The xp the stage after `stage` starts at, or undefined once fully grown.
24export const nextStageAt = (stage: PetStage): number | undefined => {
25 const index = STAGES.findIndex(step => step.stage === stage)
26
27 return STAGES[index + 1]?.from
28}
29
30// '.' is transparent; every other letter is a key of the stage's palette.
31const EGG = [
32 '..............',
33 '.....oooo.....',
34 '....obbbbo....',
35 '...obblbbbo...',
36 '...obbbbbbo...',
37 '..obbbbbllbo..',
38 '..obllbbbbbo..',
39 '..obbbbbbbbo..',
40 '..obbbblbbbo..',
41 '...obbbbbbo...',
42 '....oooooo....',
43 '..............',
44]
45
46const BODY = [
47 '..obllbbbbbo..',
48 '.obllbbbbbbbo.',
49 '.obbbbbbbbbbo.',
50 '.obbbbbbbbbbo.',
51 'obbbbbbbbbbbbo',
52 'obbbbbbbbbbbbo',
53 'obbbbbbbbbbbbo',
54 '.obbbbbbbbbbo.',
55 '..oooooooooo..',
56]
57
58const BODIES: Record<Exclude<PetStage, 'egg'>, string[]> = {
59 baby: ['..............', '.....oooo.....', '...oobbbboo...', ...BODY],
60 teen: ['..o........o..', '..obo....obo..', '..obboooobbo..', ...BODY],
61 adult: ['....g.gg.g....', '....gggggg....', '...oobbbboo...', ...BODY],
62}
63
64type Face = 'open' | 'left' | 'right' | 'closed' | 'happy'
65
66// Each eye is 2x2 pixels: top-left, top-right, bottom-left, bottom-right.
67const EYES: Record<Face, readonly [string, string, string, string]> = {
68 open: ['e', 'w', 'e', 'e'],
69 left: ['e', 'w', 'e', 'w'],
70 right: ['w', 'e', 'w', 'e'],
71 closed: ['b', 'b', 'e', 'e'],
72 happy: ['e', 'e', 'b', 'b'],
73}
74
75const MOUTHS = {
76 neutral: [[6, 9], [7, 9]],
77 smile: [[4, 8], [5, 9], [6, 9], [7, 9], [8, 9], [9, 8]],
78 frown: [[5, 9], [6, 9], [7, 9], [8, 9], [4, 10], [9, 10]],
79} as const
80
81const hex = (color: string): number => parseInt(color.slice(1), 16)
82
83const FACE_COLORS = { e: '#14201a', w: '#ffffff', m: '#14201a', p: '#ff8fab', t: '#6ec6ff' }
84
85export const PALETTES: Record<PetStage, Record<string, string>> = {
86 egg: { o: '#3b2f2f', b: '#f2e6c9', l: '#c9a36b' },
87 baby: { ...FACE_COLORS, o: '#1f5130', b: '#6fd08c', l: '#b6f2c8' },
88 teen: { ...FACE_COLORS, o: '#1d3a6b', b: '#6fa8ff', l: '#bcd6ff' },
89 adult: { ...FACE_COLORS, o: '#43205e', b: '#b07cff', l: '#dcc4ff', g: '#ffd166' },
90}
91
92const faceFor = (mood: PetMood, frame: number): Face => {
93 switch (mood) {
94 case 'happy':
95 return 'happy'
96 case 'sleeping':
97 return 'closed'
98 case 'working':
99 // Glances left and right, as if reading the code going by.
100 return Math.floor(frame / 2) % 2 === 0 ? 'left' : 'right'
101 case 'sad':
102 return 'open'
103 case 'idle':
104 return frame % 8 === 7 ? 'closed' : 'open'
105 }
106}
107
108const shift = (row: string, by: number): string =>
109 by > 0 ? '.'.repeat(by) + row.slice(0, -by) : by < 0 ? row.slice(-by) + '.'.repeat(-by) : row
110
111export const petPixels = (stage: PetStage, mood: PetMood, frame: number): string[] => {
112 if (stage === 'egg') {
113 const wobble = mood === 'working' || mood === 'happy' ? ([0, 1, 0, -1][frame % 4] ?? 0) : 0
114
115 return EGG.map(row => shift(row, wobble))
116 }
117
118 const grid = BODIES[stage].map(row => row.split(''))
119 const paint = (x: number, y: number, key: string) => {
120 const row = grid[y]
121 if (row !== undefined && x < row.length) row[x] = key
122 }
123
124 const [topLeft, topRight, bottomLeft, bottomRight] = EYES[faceFor(mood, frame)]
125 for (const x of [4, 8]) {
126 paint(x, 6, topLeft)
127 paint(x + 1, 6, topRight)
128 paint(x, 7, bottomLeft)
129 paint(x + 1, 7, bottomRight)
130 }
131
132 const mouth = mood === 'happy' ? MOUTHS.smile : mood === 'sad' ? MOUTHS.frown : MOUTHS.neutral
133 for (const [x, y] of mouth) paint(x, y, 'm')
134
135 if (mood === 'happy') {
136 paint(3, 8, 'p')
137 paint(10, 8, 'p')
138 }
139 if (mood === 'sad') paint(4, 8, 't')
140
141 return grid.map(row => row.join(''))
142}
143
144const DEFAULT_COLOR = 0x01000000
145const UPPER_HALF = 0x2580
146const LOWER_HALF = 0x2584
147const SPACE = 0x20
148
149// Packs pixel rows into RasterProps cells: [codePoint, fg, bg] u32 triplets, base64.
150export const encodeCells = (pixels: string[], palette: Record<string, string>): string => {
151 const width = pixels[0]?.length ?? 0
152 const rows = Math.floor(pixels.length / 2)
153 const words = new Uint32Array(width * rows * 3)
154 const colorAt = (x: number, y: number): number | undefined => {
155 const key = pixels[y]?.[x]
156 const color = key === undefined ? undefined : palette[key]
157
158 return color === undefined ? undefined : hex(color)
159 }
160
161 for (let y = 0; y < rows; y++) {
162 for (let x = 0; x < width; x++) {
163 const top = colorAt(x, 2 * y)
164 const bottom = colorAt(x, 2 * y + 1)
165 const cell =
166 top !== undefined
167 ? [UPPER_HALF, top, bottom ?? DEFAULT_COLOR]
168 : bottom !== undefined
169 ? [LOWER_HALF, bottom, DEFAULT_COLOR]
170 : [SPACE, DEFAULT_COLOR, DEFAULT_COLOR]
171 words.set(cell, (y * width + x) * 3)
172 }
173 }
174
175 const bytes = new Uint8Array(words.buffer)
176 let binary = ''
177 for (const byte of bytes) binary += String.fromCharCode(byte)
178
179 return btoa(binary)
180}
181
182export const drawPet = (stage: PetStage, mood: PetMood, frame: number): string =>
183 encodeCells(petPixels(stage, mood, frame), PALETTES[stage])
184
185// A text face for surfaces without Raster (desktop, VS Code, mobile).
186export const kaomoji = (stage: PetStage, mood: PetMood, frame: number): string => {
187 if (stage === 'egg') return mood === 'working' && frame % 2 === 1 ? '(0)~' : '(0)'
188
189 switch (mood) {
190 case 'happy':
191 return '(^‿^)'
192 case 'sad':
193 return '(;︵;)'
194 case 'sleeping':
195 return '(-.-)'
196 case 'working':
197 return Math.floor(frame / 2) % 2 === 0 ? '(•_• )' : '( •_•)'
198 case 'idle':
199 return frame % 8 === 7 ? '(-‿-)' : '(•‿•)'
200 }
201}
202types/index.d.ts 20 lines1export type PetStage = 'egg' | 'baby' | 'teen' | 'adult'
2
3export type PetMood = 'idle' | 'working' | 'happy' | 'sad' | 'sleeping'
4
5export type Pet = { name: string; xp: number; bornAt: number }
6
7export type PetFlash = { mood: 'happy' | 'sad'; text: string; until: number }
8
9declare module 'claude-code' {
10 interface PluginState {
11 'pixel-pet': {
12 pet: Pet | null
13 frame: number
14 flash: PetFlash | null
15 isAsleep: boolean
16 isHidden: boolean
17 }
18 }
19}
20