SLOPSHOPPER

clawd-band

An unofficial fan-made pixel cat that lives in the band above the prompt and reacts to what Claude is doing: thinking, editing, craning after a search, yowling…

newbandguardtimer
★ 1v0.1.0MITupdated 2026-09-16tomada1114/clawd-band/plugins/clawd-band
A shopper browsing a rack in a slop shop
README

clawd-band

珊瑚色のドット絵の猫が、プロンプト入力欄のすぐ上に住み着いて、Claude Code が いまやっていることに反応して動きます。

Claude Mod(function hooks)です。描画しかしません — ファイルの読み書きも、 通信も、設定の保存もしません。

非公式のファン制作物です。色と丸い体は Anthropic の Claude マスコット(通称 Clawd)に 着想を得ていますが、ドットはすべて自分で描いたもので、Anthropic とは無関係です。

      ██        ██
    ██▒▒██    ██▒▒██
      ████████████
    ████████████████
  ████░░████████░░████
██████████████████████
    ██████░░██░░██████
      ████████████
        ████████
      clawd is thinking

どう動くか

状態きっかけ見た目
idleターンとターンのあいだゆっくり息をする
sleep27 秒ほど放置耳を倒し、目を閉じ、z が流れる
thinkターン開始、未知のツール目が上を向き、ひらめきが飛ぶ
editEdit Write MultiEdit NotebookEdit右耳のわきで鉛筆を動かす
searchRead Grep Glob WebFetch WebSearch Task頭ごと左、そして右へ首を伸ばす
shellBash BashOutput KillShell耳を倒し、火花が散り、口を開けて鳴く
ouchツールがエラーを返した目をぎゅっとつぶって体が揺れる

入れる

Claude Code 2.1.273 以降 + ターミナル(デスクトップ版・IDE では出ません)。

claude plugin marketplace add tomada1114/clawd-band
claude plugin install clawd-band@clawd-band

git clone は不要です。

動かす

function hooks は early access なので、環境変数を渡して起動します。

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude

これを忘れると、エラーも警告も出ずにただ猫が出ません。 出ないときはまずここです。 毎回打つのが面倒ならエイリアスにしてください。

alias claude='CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude'

消す

claude plugin uninstall clawd-band
claude plugin marketplace remove clawd-band

設定にも $.store にも何も書き込まないので、痕跡は残りません。

描き替える

猫は plugins/clawd-band/hooks/sprites.ts に アスキーアートで直書きされています。1 フレームが 13 文字 × 9 行、1 文字が 1 ピクセル (ターミナルのセルは縦長なので、横 2 列ぶん使って描画)。

  idle: [
    [
      '...#.....#...',
      '..#i#...#i#..',
      '...#*#####...',
      '..#########..',
      '.##e#####e##.',
      'n#####m#####n',
      '..###m#m###..',
      '...#######...',
      '....%%%%%....',
    ],
    // ...
  ],
  .  透過      #  体      *  つや      %  影      i  耳の内側    n  ひげ
  e  目        x  つぶった目           m  鼻と口  o  開いた口
  z  寒色(zzz、虫眼鏡)   y  暖色(火花、鉛筆)   r  警告

文字を書き換えて再起動すれば絵が変わります。PNG も変換スクリプトもビルドもありません。 耳と頭はアートそのものなので、耳を倒したり頭ごと傾けたりできます。 状態を足すときは MascotState FRAMES CAPTIONS CAPTION_COLORS の 4 か所に。

clone して直接動かす場合:

git clone https://github.com/tomada1114/clawd-band.git
cd clawd-band
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir plugins/clawd-band

仕組み

AbovePrompt の帯に Raster(セルのグリッド)を 1 枚置き、$.ui.blit でその場で 塗り替えています。再描画が要るのはキャプションも変わる状態遷移のときだけです。 状態機械は turn.start / turn.complete / tool.call / session.start の 4 フック。

claude plugin validate plugins/clawd-band   # 触る API が全部出ます
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude plugin test plugins/clawd-band

ライセンス

MIT

Source 2 files
hooks/register.tsx 169 lines
1/* @jsxRuntime classic */
2/* @jsx h */
3/* @jsxFrag Fragment */
4
5// Never name a local `h` in this file: every JSX tag compiles to a call of `h`,
6// which the engine injects as an ambient global.
7
8import type { EngineInterface, On } from 'claude-code'
9import {
10  CAPTIONS,
11  CAPTION_COLORS,
12  cellsFor,
13  COLUMNS,
14  FRAMES,
15  ROWS,
16  type MascotState,
17} from './sprites.ts'
18
19/** How long one frame is held. The band may repaint 30 times a second. */
20const FRAME_MS = 180
21
22/** Ticks of nobody asking anything before the blob dozes off (~27s). */
23const SLEEP_AFTER_TICKS = 150
24
25/** Ticks a flinch is held before the blob goes back to whatever it was doing. */
26const OUCH_TICKS = 9
27
28/** The `Raster`'s address for `$.ui.blit`. */
29const RASTER_KEY = 'clawd'
30
31/** Which tools read the world, which ones change it, which ones are a shell. */
32const TOOL_STATES: Readonly<Record<string, MascotState>> = {
33  Edit: 'edit',
34  MultiEdit: 'edit',
35  Write: 'edit',
36  NotebookEdit: 'edit',
37  Read: 'search',
38  Grep: 'search',
39  Glob: 'search',
40  WebFetch: 'search',
41  WebSearch: 'search',
42  Task: 'search',
43  Bash: 'shell',
44  BashOutput: 'shell',
45  KillShell: 'shell',
46}
47
48// The mascot's whole mind. Module scope, not `register`'s closure, because the
49// static scan the engine runs before loading only lets `$` reach a function
50// declared at the top of the file.
51let state: MascotState = 'idle'
52let frame = 0
53let quietTicks = 0
54let ouchTicks = 0
55let isWorking = false
56/** What to go back to once a flinch or a turn is over. */
57let working: MascotState = 'think'
58/** The band's current draw, so a blit knows where to land. */
59let requestId: string | null = null
60let ticking = false
61
62function currentFrame(): readonly string[] {
63  const frames = FRAMES[state]
64  return frames[frame % frames.length] ?? frames[0]!
65}
66
67/**
68 * A mounted Raster is repainted in place — no redraw, so the animation costs
69 * nothing but the cells.
70 */
71function paint($: EngineInterface): void {
72  if (requestId === null) return
73  void $.ui
74    .blit({ requestId, key: RASTER_KEY, cells: cellsFor(currentFrame()) })
75    .catch(() => {})
76}
77
78/** The caption is an element, so a state change asks for a real redraw too. */
79function transition($: EngineInterface, next: MascotState): void {
80  if (state === next) return
81  state = next
82  frame = 0
83  quietTicks = 0
84  $.ui.invalidate('ui.render')
85  paint($)
86}
87
88function tick($: EngineInterface): void {
89  frame += 1
90  if (ouchTicks > 0) {
91    ouchTicks -= 1
92    if (ouchTicks === 0) {
93      // Back to work, or back to waiting for the next prompt.
94      transition($, isWorking ? working : 'idle')
95      return
96    }
97  } else if (state === 'idle') {
98    quietTicks += 1
99    if (quietTicks >= SLEEP_AFTER_TICKS) {
100      transition($, 'sleep')
101      return
102    }
103  }
104  paint($)
105}
106
107export function register(on: On) {
108  on('session.start', ($, e, next) => {
109    if (!ticking) {
110      ticking = true
111      $.clock.every(FRAME_MS, () => tick($))
112    }
113    return next(e)
114  })
115
116  on('turn.start', ($, e, next) => {
117    isWorking = true
118    working = 'think'
119    if (ouchTicks === 0) transition($, 'think')
120    return next(e)
121  })
122
123  on('turn.complete', ($, e, next) => {
124    isWorking = false
125    quietTicks = 0
126    if (ouchTicks === 0) transition($, 'idle')
127    return next(e)
128  })
129
130  on('tool.call', async ($, e, next) => {
131    working = TOOL_STATES[e.tool] ?? 'think'
132    // A flinch owns the blob until it wears off, however busy things get.
133    if (ouchTicks === 0) transition($, working)
134
135    const result = await next(e)
136
137    if (result.isError === true) {
138      ouchTicks = OUCH_TICKS
139      transition($, 'ouch')
140    }
141    return result
142  })
143
144  on('ui.render', { component: 'AbovePrompt' }, ($, e, next) => {
145    // Other surfaces draw their own band, a survey owns it when one is up, and
146    // a band too short for the blob is better left alone.
147    if (e.surface !== 'terminal' || e.props.hasSurvey) return next(e)
148    if (e.props.maxRows < ROWS + 1) return next(e)
149
150    requestId = e.requestId
151
152    const { Box, Text, Raster } = $.ui.resolve(e)
153
154    return (
155      <Box flexDirection="column">
156        <Raster
157          key={RASTER_KEY}
158          columns={COLUMNS}
159          rows={ROWS}
160          cells={cellsFor(currentFrame())}
161        />
162        <Text color={CAPTION_COLORS[state]} dimColor={state === 'sleep'}>
163          {CAPTIONS[state]}
164        </Text>
165      </Box>
166    )
167  })
168}
169
hooks/sprites.ts 309 lines
1// The whole mascot, drawn as text.
2//
3// Every frame is nine rows of thirteen characters. One character is one pixel;
4// a pixel is drawn two terminal columns wide, because a terminal cell is about
5// twice as tall as it is wide and the blob wants to be round. Edit the art
6// below and the mascot changes — there is no PNG, no build step, no generator.
7//
8//   .  nothing (the terminal's own background shows through)
9//   #  body            *  sheen               %  body shade
10//   i  inner ear       n  whisker
11//   e  eye             x  dizzy eye           m  nose and mouth      o  open mouth
12//   z  cool accent (zzz, the magnifier)       y  warm accent (sparks, pencil)
13//   r  alarm accent
14//
15// The ears and the head are part of the art, not an overlay, so a state can
16// fold the ears back, perk them up, or lean the whole head to one side.
17
18/** A colour as `Raster` wants it: 0x00RRGGBB. */
19type Rgb = number
20
21/** `Raster`'s sentinel for "whatever the terminal already uses". */
22const DEFAULT_COLOR = 0x01000000
23
24const FULL_BLOCK = 0x2588
25const SPACE = 0x20
26
27/** Coral, because the mascot this one is a fan of is coral. */
28export const PALETTE: Readonly<Record<string, Rgb>> = {
29  '#': 0xd97757,
30  '*': 0xf3b394,
31  '%': 0xbe6144,
32  i: 0xe89a8c,
33  n: 0xffe4d6,
34  e: 0x2b1a14,
35  x: 0x2b1a14,
36  m: 0x5e2317,
37  o: 0x3d1710,
38  z: 0x8fb8d8,
39  y: 0xf5c542,
40  r: 0xe05252,
41}
42
43/** Pixels across and down in the art, before the 2x horizontal stretch. */
44export const ART_COLUMNS = 13
45export const ART_ROWS = 9
46
47/** How many terminal cells wide one art pixel is drawn. */
48export const PIXEL_WIDTH = 2
49
50export const COLUMNS = ART_COLUMNS * PIXEL_WIDTH
51export const ROWS = ART_ROWS
52
53/** What the mascot can be doing. */
54export type MascotState =
55  | 'idle'
56  | 'sleep'
57  | 'think'
58  | 'edit'
59  | 'search'
60  | 'shell'
61  | 'ouch'
62
63export type Frame = readonly string[]
64
65/**
66 * The flipbook. Frames cycle in order at one tick each, so a two-frame state
67 * breathes and a three-frame state drifts.
68 */
69export const FRAMES: Readonly<Record<MascotState, readonly Frame[]>> = {
70  // Between turns: ears up, a slow breath in and out.
71  idle: [
72    [
73      '...#.....#...',
74      '..#i#...#i#..',
75      '...#*#####...',
76      '..#########..',
77      '.##e#####e##.',
78      'n#####m#####n',
79      '..###m#m###..',
80      '...#######...',
81      '....%%%%%....',
82    ],
83    [
84      '...#.....#...',
85      '..#i#...#i#..',
86      '..#*#######..',
87      '.###########.',
88      '.##e#####e##.',
89      'n#####m#####n',
90      '.####m#m####.',
91      '..#########..',
92      '...%%%%%%%...',
93    ],
94  ],
95
96  // Left alone long enough: ears folded, eyes shut, a z drifting off.
97  sleep: [
98    [
99      '.........z...',
100      '..##.....##..',
101      '...#*#####...',
102      '..#########..',
103      '.##ee###ee##.',
104      'n#####m#####n',
105      '..#########..',
106      '...#######...',
107      '....%%%%%....',
108    ],
109    [
110      '..........z..',
111      '..##.....##..',
112      '...#*#####...',
113      '..#########..',
114      '.##ee###ee##.',
115      'n#####m#####n',
116      '..#########..',
117      '...#######...',
118      '....%%%%%....',
119    ],
120    [
121      '.............',
122      '..##.....##..',
123      '..#*#######..',
124      '.###########.',
125      '.##ee###ee##.',
126      'n#####m#####n',
127      '.###########.',
128      '..#########..',
129      '...%%%%%%%...',
130    ],
131  ],
132
133  // Working, nothing more specific: eyes roll up, ideas pop overhead.
134  think: [
135    [
136      '...#..z..#...',
137      '..#i#...#i#..',
138      '...#*#####...',
139      '..#e#####e#..',
140      '.###########.',
141      'n#####m#####n',
142      '..###m#m###..',
143      '...#######...',
144      '....%%%%%....',
145    ],
146    [
147      '...#.z.z.#...',
148      '..#i#...#i#..',
149      '...#*#####...',
150      '..#########..',
151      '.##e#####e##.',
152      'n#####m#####n',
153      '..###m#m###..',
154      '...#######...',
155      '....%%%%%....',
156    ],
157  ],
158
159  // Edit / Write: a pencil stub jabbing past the right ear.
160  edit: [
161    [
162      '...#.....#...',
163      '..#i#...#i#..',
164      '...#*#####...',
165      '..#########y.',
166      '.##e#####e#y.',
167      'n#####m#####n',
168      '..###m#m###..',
169      '...#######...',
170      '....%%%%%....',
171    ],
172    [
173      '...#.....#...',
174      '..#i#...#i#..',
175      '...#*#####.y.',
176      '..#########y.',
177      '.##e#####e##.',
178      'n#####m#####n',
179      '..###m#m###..',
180      '...#######...',
181      '....%%%%%....',
182    ],
183  ],
184
185  // Grep / Glob / Read: the whole head cranes left, then right, after the glass.
186  search: [
187    [
188      '..#.....#..z.',
189      '.#i#...#i#...',
190      '..#*#####....',
191      '.#########...',
192      '##e#####e##..',
193      'n#####m#####n',
194      '..###m#m###..',
195      '...#######...',
196      '....%%%%%....',
197    ],
198    [
199      '.z..#.....#..',
200      '...#i#...#i#.',
201      '....#*#####..',
202      '...#########.',
203      '..##e#####e##',
204      'n#####m#####n',
205      '..###m#m###..',
206      '...#######...',
207      '....%%%%%....',
208    ],
209  ],
210
211  // Bash: ears back, sparks off the top, and a yowl on the second frame.
212  shell: [
213    [
214      '..y.y...y.y..',
215      '..##.....##..',
216      '...#*#####...',
217      '..#########..',
218      '.##e#####e##.',
219      'n#####m#####n',
220      '..###m#m###..',
221      '...#######...',
222      '....%%%%%....',
223    ],
224    [
225      '.y.y.y.y.y.y.',
226      '..##.....##..',
227      '...#*#####...',
228      '..#########..',
229      '.##e#####e##.',
230      'n#####m#####n',
231      '..###ooo###..',
232      '...#######...',
233      '....%%%%%....',
234    ],
235  ],
236
237  // A tool came back with an error: ears flat, eyes crossed, the cat wobbles.
238  ouch: [
239    [
240      '.r.........r.',
241      '..##.....##..',
242      '...#######...',
243      '..#x#####x#..',
244      '.###x###x###.',
245      '###x#####x###',
246      '..###ooo###..',
247      '...#######...',
248      '....%%%%%....',
249    ],
250    [
251      'r.........r..',
252      '...##.....##.',
253      '....#######..',
254      '...#x#####x#.',
255      '..###x###x###',
256      '.###x#####x##',
257      '...###ooo###.',
258      '....#######..',
259      '.....%%%%%...',
260    ],
261  ],
262}
263
264/** The line under the mascot. */
265export const CAPTIONS: Readonly<Record<MascotState, string>> = {
266  idle: 'clawd is waiting',
267  sleep: 'clawd is asleep  z z z',
268  think: 'clawd is thinking',
269  edit: 'clawd is editing',
270  search: 'clawd is looking around',
271  shell: 'clawd is in the shell',
272  ouch: 'ouch! that tool bit back',
273}
274
275/** The caption colour, so the line matches whatever the blob is up to. */
276export const CAPTION_COLORS: Readonly<Record<MascotState, string>> = {
277  idle: '#d97757',
278  sleep: '#8fb8d8',
279  think: '#8fb8d8',
280  edit: '#f5c542',
281  search: '#8fb8d8',
282  shell: '#f5c542',
283  ouch: '#e05252',
284}
285
286/**
287 * One frame's art to the base64 `cells` a `Raster` takes: `columns * rows`
288 * little-endian u32 triplets of `[codePoint, foreground, background]`.
289 */
290export function cellsFor(frame: Frame): string {
291  const words = new Uint32Array(COLUMNS * ROWS * 3)
292  for (let y = 0; y < ROWS; y++) {
293    const row = frame[y] ?? ''
294    for (let ax = 0; ax < ART_COLUMNS; ax++) {
295      const paint = PALETTE[row[ax] ?? '.']
296      for (let sub = 0; sub < PIXEL_WIDTH; sub++) {
297        const i = (y * COLUMNS + ax * PIXEL_WIDTH + sub) * 3
298        words[i] = paint === undefined ? SPACE : FULL_BLOCK
299        words[i + 1] = paint === undefined ? DEFAULT_COLOR : paint
300        words[i + 2] = DEFAULT_COLOR
301      }
302    }
303  }
304  // Uint8Array.prototype.toBase64 is in the hooks environment but not in the
305  // es2023 lib the tsconfig names, so the cast is the whole of the workaround.
306  const bytes = new Uint8Array(words.buffer) as unknown as { toBase64: () => string }
307  return bytes.toBase64()
308}
309