SLOPSHOPPER

matrix-rain

Matrix digital rain: a /matrix pane, and a few rows above the prompt while Claude works. Pure eye candy.

newpanebandcommandprompttimer
v0.1.0MITupdated 2026-10-06an80sPWNstar/claude-mods/matrix-rain
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · matrix-rain
│ ┃ Matrix ✕ › fix the failing auth test and add an audit log call │ ┃ レ フ " シ ヲ チ ー ア … │ ┃ ッ ソ ァ ー : ッ : … ⏺ Read(src/auth.ts) │ ┃ レ 6 ヘ ク タ ニ … ⎿ Read 6 lines │ ┃ オ ョ 4 2 ル 7 … ⏺ Update(src/auth.ts) │ ┃ ホ 7 ニ ノ モ リ … ⎿ Added 2 lines, removed 1 line │ ┃ ヤ ワ フ ラ リ < ァ … ⏺ Bash(bun test) │ ┃ ム 0 チ セ 0 ホ コ |… ⎿ 3 pass, 1 fail │ ┃ ソ ホ 4 ツ ァ ァ ケ… │ ┃ - ア ノ カ ホ ヨ ミ - 0… ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ チ ソ ァ ヘ ク タ ホ リ ニ… │ ┃ ン ロ 2 チ ヒ ハ ワ 1 ノ… ✻ Worked for 42s · done 4:20 PM │ ┃ レ ム ォ タ 0 セ ェ テ .… │ ┃ " ソ ン タ 8 ォ ウ ム ハ… › /matrix │ ┃ カ ラ = ラ ャ 4 シ 0 ヲ… ⎿ matrix-rain: Entering the Matrix. /matrix again to leave, /matri │ ┃ ラ ゥ ツ ア ワ 3 ヘ イ… │ ┃ ラ カ 7 ィ . ッ ッ… │ ┃ ィ ロ ハ ツ ル ア リ… │ ┃ テ リ 3 3 メ ゥ 5… │ ┃ エ フ ゥ : ホ カ… │ ┃ ナ ワ ニ ュ… │ ┃ 6 ナ "… │ ┃ ェ ト 6… │ ┃ | : ム… │ ┃ ゥ 9… │ ┃ < ゥ… タ メ タ ソ ワ ネ " > ユ 4 オ 5 リ ヒ ソ リ モ | ト オ ュ ツ セ サ エ | ロ フ フ ツ ハ ヨ ム ロ キ + チ ィ ゥ オ ツ オ 6 : 1 ヤ ォ ナ - ァ ル タ 6 ヲ イ リ ヤ モ < ハ ュ コ テ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
タ メ タ ソ ワ ネ " > ユ 4 オ 5 リ ヒ ソ リ モ | ト オ ュ ツ セ サ エ | ロ フ フ ツ ハ ヨ ム ロ キ + チ ィ ゥ オ ツ オ 6 : 1 ヤ ォ ナ - ァ ル タ 6 ヲ イ リ ヤ モ < ハ ュ コ テ
Pane · Matrix
レ フ " シ ヲ チ ー ア = 6 ッ ソ ァ ー : ッ : ツ 9 レ 6 ヘ ク タ ニ ッ 6 オ ョ 4 2 ル 7 9 ワ ホ 7 ニ ノ モ リ ー > ヤ ワ フ ラ リ < ァ マ ォ ム 0 チ セ 0 ホ コ | ラ ソ ソ ホ 4 ツ ァ ァ ケ キ : - ア ノ カ ホ ヨ ミ - 0 ム ャ チ ソ ァ ヘ ク タ ホ リ ニ ノ エ ン ロ 2 チ ヒ ハ ワ 1 ノ オ ロ レ ム ォ タ 0 セ ェ テ . ヌ 5 " ソ ン タ 8 ォ ウ ム ハ ケ ハ カ ラ = ラ ャ 4 シ 0 ヲ ン ネ ラ ゥ ツ ア ワ 3 ヘ イ エ ラ カ 7 ィ . ッ ッ ヲ ィ ロ ハ ツ ル ア リ > テ リ 3 3 メ ゥ 5 | エ フ ゥ : ホ カ エ ナ ワ ニ ュ 6 6 ナ " セ ェ ト 6 7 | : ム ゥ 9 < ゥ ハ 1 ツ テ
README

claude-mods

A Claude Code plugin marketplace for Claude Mods. Mods are built from function hooks and are GA in Claude Code v2.1.287 (on by default). Each mod is a folder: token-meter and matrix-rain. More mods will be added as folders.

Install

Run these commands in Claude Code:

/plugin marketplace add an80sPWNstar/claude-mods
/plugin install token-meter@claude-mods
/plugin install matrix-rain@claude-mods

Install either one or both.

Requires Claude Code 2.1.287 or newer. Built and tested in the terminal CLI (fullscreen layout, pane docked beside the transcript).

token-meter

Shows how many tokens THIS session has used, Claude and local LLMs side by side, as plain numbers (no percentages, no account-wide usage).

The /tokens pane: SCORE, the Pac-Man context maze, and the per-model and per-box tables

  1. Status line: Claude 22.51M (22.19M cached) · Local 261.7k. Claude = input + output + cache read + cache write for every model step in the session. The number in parentheses is the cache-read part.
  2. Spinner: while Claude works, the spinner line ends with … · Claude +18.4k this turn (tokens since your last message).
  3. /tokens pane:
  4. SCORE 315,335: fresh tokens this session (input + output + cache write; cache reads not counted).
  5. A Pac-Man maze: 200 peach dots in 10 rows of 20, one dot per 5k tokens of a 1M context window. Pac-Man's position is how full the context is right now (size of the latest main-thread request). He snakes left to right, then right to left, eats his way forward as the context grows, chomps while fresh tokens are flowing, and the board refills when the conversation is compacted. Below it: 1 dot = 5k context · 174.8k of 1M.
  6. A table per Claude model (requests, input, cache write, cache read, output) and per local box (jobs, total), then a Total line Claude 22.51M · Local 261.7k. Below 54 columns of pane width the table switches to narrower columns.

Local models

token-meter counts local LLM usage by reading the job records of lanllm (https://github.com/an80sPWNstar/lanllm), a Claude Code plugin by the same author that lets Claude delegate work to local LLM servers (llama.cpp boxes on your LAN). token-meter does not talk to local models itself. It reads ~/.lanllm/jobs/<session-id>/*.json every 20 seconds and adds up prompt + completion tokens per box. lanllm files each job under the Claude Code session id (it reads the CLAUDE_CODE_SESSION_ID variable), so only this session's local work is counted. Without lanllm, the Local numbers stay at 0 and everything else works.

Subagent guard

Opinionated: when Claude launches a subagent:

  • no model given: it is rewritten to Haiku;
  • a model other than Haiku, or a fork: the launch is refused once with the reason "routing rule is Haiku first unless Haiku already failed at this task"; issuing the identical call again lets it through;
  • Haiku and agent-team teammates pass untouched.

If you do not want this, the guard is the agent.spawn hook in token-meter/hooks/register.tsx.

Tuning

Constants at the top of token-meter/hooks/tally.ts: CTX_WINDOW (1000000), CTX_DOT (5000 tokens per dot), CTX_DOTS (200), ROW (20 dots per row), ROWS (10). They are separate literals, so change them together (CTX_DOTS = CTX_WINDOW / CTX_DOT, ROWS = CTX_DOTS / ROW), update the 5k in ctxLabel, and update tests/ctx.test.ts. Colors are in toneProps in token-meter/hooks/register.tsx: amber #e8a33d (Claude), green #6bcb77 (local), peach #ffb8ae (dots), bold yellow #ffff00 (Pac-Man).

What it touches

Reads: the env vars USERPROFILE / HOME (to find ~/.lanllm), files under ~/.lanllm/jobs/<session-id>/. Writes: only its own plugin store (session totals and the last context size, so a reload keeps the counts). No network, no processes.

matrix-rain

Matrix digital rain in green katakana-style glyphs. Pure eye candy, no effect on Claude.

  • /matrix opens a docked rain pane (asks for 48 columns); /matrix again closes it.
  • Band: while Claude is working, a few rows of rain fall above the prompt and stop when the turn ends. On by default; /matrix band toggles it, and the choice is remembered across sessions.

What it touches: only its own plugin store (the band on/off setting). No files, no network, no processes.

Development

The tests cover the pure functions (token-meter/hooks/tally.ts, matrix-rain/hooks/rain.ts). From the repo root:

claude plugin validate token-meter
claude plugin test token-meter
claude plugin validate matrix-rain
claude plugin test matrix-rain

Type-check with tsc -p token-meter (or matrix-rain) after Claude Code has loaded the mod once (it writes .claude-plugin/types/, which is git-ignored).

License

MIT

Source 2 files
hooks/register.tsx 130 lines
1import type { Register } from 'claude-code'
2import { createRain, stepRain, fitRain, renderRain, BAND_ROWS } from './rain'
3import type { Rain, Run, Tone } from './rain'
4
5const PANE = 'matrix-rain'
6let paneOpen = false
7let bandOn = true
8let working = false
9let paneRain: Rain | null = null
10let bandRain: Rain | null = null
11
12function toneProps(tone: Tone): { color?: string; bold?: boolean } {
13  switch (tone) {
14    case 'head': return { color: '#e8ffe8', bold: true }
15    case 'bright': return { color: '#00ff41' }
16    case 'mid': return { color: '#00b32c' }
17    case 'dim': return { color: '#005f1a' }
18    case 'blank': return {}
19  }
20}
21
22export const register: Register = on => {
23  on('session.start', async ($, e, next) => {
24    const saved = await $.store.get('band')
25    if (typeof saved === 'boolean') bandOn = saved
26
27    await $.command.register({
28      name: 'matrix',
29      description: 'Matrix rain: /matrix toggles the pane, /matrix band toggles rain while Claude works'
30    })
31
32    $.clock.every(80, () => {
33      let changed = false
34      if (paneOpen) {
35        if (paneRain) stepRain(paneRain)
36        changed = true
37      }
38      if (bandOn && working) {
39        if (bandRain) stepRain(bandRain)
40        changed = true
41      }
42      if (changed) {
43        $.ui.invalidate('ui.render')
44      }
45    })
46
47    return next(e)
48  })
49
50  on('prompt.submit', async ($, e, next) => {
51    working = true
52    return next(e)
53  })
54
55  on('turn.complete', async ($, e, next) => {
56    working = false
57    $.ui.invalidate('ui.render')
58    return next(e)
59  })
60
61  on('ui.close', async ($, e, next) => {
62    if (e.id === PANE) {
63      paneOpen = false
64      paneRain = null
65    }
66    return next(e)
67  })
68
69  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
70    const { Box, Text } = $.ui.resolve(e)
71    paneOpen = true
72    paneRain = fitRain(paneRain, e.props.bodyColumns, e.props.scroll.bodyRows, Date.now() | 0)
73    const lines = renderRain(paneRain)
74    return (
75      <Box flexDirection="column">
76        {lines.map(line => (
77          <Text wrap="truncate-end">
78            {line.map((r: Run) => (
79              <Text {...toneProps(r.tone)}>{r.text}</Text>
80            ))}
81          </Text>
82        ))}
83      </Box>
84    )
85  })
86
87  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
88    const { Box, Text } = $.ui.resolve(e)
89    if (!bandOn || !e.props.isWorking || e.props.hasSurvey) {
90      bandRain = null
91      return next(e)
92    }
93    bandRain = fitRain(bandRain, e.props.bodyColumns, Math.min(BAND_ROWS, e.props.maxRows), Date.now() | 0)
94    const lines = renderRain(bandRain)
95    return (
96      <Box flexDirection="column">
97        {lines.map(line => (
98          <Text wrap="truncate-end">
99            {line.map((r: Run) => (
100              <Text {...toneProps(r.tone)}>{r.text}</Text>
101            ))}
102          </Text>
103        ))}
104      </Box>
105    )
106  })
107
108  on('command.run', { command: 'matrix' }, async ($, e) => {
109    const arg = e.args.trim().toLowerCase()
110    if (arg === 'band') {
111      bandOn = !bandOn
112      await $.store.set('band', bandOn)
113      $.ui.invalidate('ui.render')
114      return { text: bandOn ? 'Rain while Claude works: on.' : 'Rain while Claude works: off.' }
115    }
116
117    const open = (await $.ui.panes()).some(p => p.id === PANE)
118    if (open) {
119      await $.ui.close({ id: PANE })
120      paneOpen = false
121      paneRain = null
122      return { text: 'Left the Matrix.' }
123    } else {
124      await $.ui.open({ id: PANE, title: 'Matrix', columns: 48 })
125      paneOpen = true
126      return { text: 'Entering the Matrix. /matrix again to leave, /matrix band toggles rain while Claude works.' }
127    }
128  })
129}
130
hooks/rain.ts 124 lines
1export type Tone = 'head' | 'bright' | 'mid' | 'dim' | 'blank'
2export type Run = { text: string; tone: Tone }
3export type Drop = { head: number; len: number; speed: number; wait: number }
4export type Rain = { width: number; rows: number; cols: number; tick: number; seed: number; drops: Drop[]; grid: string[][] }
5
6const katakana = (() => {
7  let s = ''
8  for (let i = 0; i < 56; i++) s += String.fromCharCode(0xff66 + i)
9  return s
10})()
11
12export const GLYPHS: string = katakana + '0123456789' + 'Z:."=*+-<>|'
13export const GAP = 2
14export const BAND_ROWS = 4
15
16export function nextRand(seed: number): [number, number] {
17  const s = (seed + 0x6D2B79F5) | 0
18  let t = Math.imul(s ^ (s >>> 15), 1 | s)
19  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
20  return [((t ^ (t >>> 14)) >>> 0) / 4294967296, s]
21}
22
23function randInt(rain: Rain, lo: number, hi: number): number {
24  const [v, s] = nextRand(rain.seed)
25  rain.seed = s
26  return lo + Math.floor(v * (hi - lo + 1))
27}
28
29function glyph(rain: Rain): string {
30  return GLYPHS[randInt(rain, 0, GLYPHS.length - 1)] ?? ''
31}
32
33export function columnsFor(width: number): number {
34  return Math.max(1, Math.floor(width / GAP))
35}
36
37export function toneAt(d: number, len: number): Tone {
38  if (d < 0 || d >= len) return 'blank'
39  if (d === 0) return 'head'
40  if (d <= Math.floor(len / 3)) return 'bright'
41  if (d <= Math.floor((2 * len) / 3)) return 'mid'
42  return 'dim'
43}
44
45function newDrop(rain: Rain, initial: boolean): Drop {
46  const len = randInt(rain, 4, Math.max(5, rain.rows))
47  const speed = randInt(rain, 1, 3)
48  if (initial) {
49    const head = randInt(rain, -rain.rows, rain.rows - 1)
50    return { head, len, speed, wait: 0 }
51  }
52  const wait = randInt(rain, 0, rain.rows)
53  return { head: -1, len, speed, wait }
54}
55
56export function createRain(width: number, rows: number, seed: number): Rain {
57  const r = Math.max(1, rows)
58  const cols = columnsFor(width)
59  const rain: Rain = { width, rows: r, cols, tick: 0, seed, drops: [], grid: [] }
60  for (let i = 0; i < r; i++) {
61    const row: string[] = []
62    for (let j = 0; j < cols; j++) row.push(glyph(rain))
63    rain.grid.push(row)
64  }
65  for (let c = 0; c < cols; c++) rain.drops.push(newDrop(rain, true))
66  return rain
67}
68
69export function stepRain(rain: Rain): void {
70  rain.tick += 1
71  for (let c = 0; c < rain.cols; c++) {
72    const d = rain.drops[c]
73    if (!d) continue
74    if (d.wait > 0) {
75      d.wait -= 1
76      continue
77    }
78    if (rain.tick % d.speed === 0) {
79      d.head += 1
80      if (d.head >= 0 && d.head < rain.rows) {
81        const row = rain.grid[d.head]
82        if (row) row[c] = glyph(rain)
83      }
84    }
85    if (d.head - d.len >= rain.rows) {
86      rain.drops[c] = newDrop(rain, false)
87    }
88  }
89  const n = Math.max(1, Math.floor((rain.cols * rain.rows) / 40))
90  for (let i = 0; i < n; i++) {
91    const r = randInt(rain, 0, rain.rows - 1)
92    const c = randInt(rain, 0, rain.cols - 1)
93    const row = rain.grid[r]
94    if (row) row[c] = glyph(rain)
95  }
96}
97
98export function fitRain(rain: Rain | null, width: number, rows: number, seed: number): Rain {
99  if (rain !== null && rain.width === width && rain.rows === Math.max(1, rows)) return rain
100  return createRain(width, rows, seed)
101}
102
103export function renderRain(rain: Rain): Run[][] {
104  const lines: Run[][] = []
105  for (let r = 0; r < rain.rows; r++) {
106    const line: Run[] = []
107    for (let c = 0; c < rain.cols; c++) {
108      const d = (rain.drops[c]?.head ?? 0) - r
109      const len = rain.drops[c]?.len ?? 0
110      const tone = toneAt(d, len)
111      const g = rain.grid[r]?.[c] ?? ''
112      const text = tone === 'blank' ? '  ' : g + ' '
113      const last = line[line.length - 1]
114      if (last && last.tone === tone) {
115        last.text += text
116      } else {
117        line.push({ text, tone })
118      }
119    }
120    lines.push(line)
121  }
122  return lines
123}
124