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

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.
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).
Shows how many tokens THIS session has used, Claude and local LLMs side by side, as plain numbers (no percentages, no account-wide usage).

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.… · Claude +18.4k this turn (tokens since your last message)./tokens pane:SCORE 315,335: fresh tokens this session (input + output + cache write; cache reads not counted).1 dot = 5k context · 174.8k of 1M.Claude 22.51M · Local 261.7k. Below 54 columns of pane width the table switches to narrower columns.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.
Opinionated: when Claude launches a subagent:
If you do not want this, the guard is the agent.spawn hook in token-meter/hooks/register.tsx.
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).
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 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./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.
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).
MIT
hooks/register.tsx 130 lines1import 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}
130hooks/rain.ts 124 lines1export 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