Glitch kit core: shared themes and the /glitch command.

Glitch is a kit of Claude Code mods, published from this folder as a marketplace named glitch. It puts one band above the prompt that shows the session at a glance, makes spending and prompt-cache expiry visible before they cost money, and themes everything from one place. A Glitch bug never breaks the session, and Glitch refuses nothing unless you ask it to: the one refusal is the opt-in subagent block at a budget cap.
| Plugin | What it does |
|---|---|
glitch-core | Shared themes and the /glitch command. |
glitch-hud | The band above the prompt: turn, speed, cost and budget, cache countdown with keep-warm, context, git. Depends on glitch-core. |
glitch-pets | A pixel pet at the end of the HUD row, with moods, short remarks and a daily streak. Depends on glitch-core and glitch-hud. |
One line above the prompt. In the terminal it is text with theme colors and icons:
| Part | Example | Source |
|---|---|---|
| turn | #12 | count of main-session turns |
| speed | ⚡ 84 tok/s | output tokens divided by streaming time of the last main response (ignores answers under 20 tokens or 0.5 s) |
| cost | $ 0.42 reply · 3.10 session · 21.40 today | the last reply, this session, and today across every session on this machine; pick them with costShows. Colored by budget level |
| cache | ⧗ 47:12 ↻ | countdown to prompt-cache expiry (cacheTtlMinutes, 60 by default); … while a turn runs |
| context | ▓▓▓▓░░ 63% | context window used; amber from 65%, red from 85% |
| git | main ±4 ↑1 | branch, changed files, ahead/behind; hidden outside a repo |
Desktop, VS Code and mobile. These apps draw the band with vector icons, a real context bar and worded values (Turn 12, $0.42 reply · $3.10 session · $21.40 today, 47:12 left, 63% context). The desktop already shows the branch above the band, so git is left out there unless you change Parts in the settings or run /glitch hud parts ….
The three costs. A reply is everything Claude does to answer one message: every model call, tool use and subagent until it stops. It is left out while it equals the session figure (the first reply). Session is the figure /cost shows. Today adds up every session that ran glitch-hud on this computer since local midnight, in any project; it is not your account's spend on other machines or on claude.ai. With a cap set, the figure shows it: $3.10 of $5.00 session.
Width. As the window narrows, parts drop in the order git, speed, turn; then the cost figures lose their words and the context bar becomes just the percent. The row never wraps.
Hiding. The × at the end of the band hides it for the session; /glitch hud toggles it again. The band is not drawn before the first turn or while a survey shows. Other mods' bands render beneath it.
Set a cap per session (budgetSession), per local day (budgetDaily), or both. 0 turns a cap off.
The 90% and 100% toasts appear in the session whatever notify says; notify only picks how the 100% notification (and the cache warnings) reach you, and off drops those.
Each level fires once per cap per session; raising a cap re-arms the higher levels. The daily ledger sums every session's spend for the local date: each session keeps its own entry per day under its session id, so sessions running side by side, a reload, a /clear or a resume never double count; entries older than 7 days are pruned. Caps changed in /config apply when the plugin reloads, except a session cap you already set with /glitch budget.
With budgetBlocksAgents on, new subagents are refused once either cap is reached. At the session cap the refusal asks Claude to have you raise it with /glitch budget +N; at the daily cap it asks you to run /glitch budget off for this session or raise budgetDaily in /config (+N moves the session cap only). The main conversation is never blocked, and if the check itself fails the call is allowed.
Costs are Claude Code's own estimate at API prices, not a bill; on a subscription they are not charged.
Claude Code's prompt cache lives for a fixed time after the last request: 1 hour on most plans, 5 minutes on others. Set cacheTtlMinutes to match (60 by default; set 5 if your account's cache lasts 5 minutes). The cache part counts down to that deadline from each answer and pauses (…) while a turn runs; a turn interrupted before any request leaves it as it was, and /clear drops it.
Left empty, cacheWarnMinutes fits the lifetime: marks at 10 and 5 minutes left for 1 hour, at 2 and 1 for 5 minutes. Marks you type at or above the lifetime can never fire: they are ignored and named once at session start. The countdown turns the warning color at a third of the lifetime left (20 minutes of an hour, 1:40 of 5 minutes) and the alert color at a sixth (10 minutes, 0:50).
<project> — Glitch: "Prompt cache expires in N min".notify = windows, a native Windows toast is tried first; if that fails (or the OS is not Windows) Claude Code's own notification channel is used, then an in-app toast. toast goes straight to the in-app toast; off turns off the cache warnings and the budget-cap notification (the budget toasts above still show).↻ on the cache part, or /glitch warm, sends a tool-less fork of the session's last request with a one-line question after it. The request is served from the prompt cache, which refreshes it, and nothing is added to the transcript. It is refused while a turn runs. The reply reports how many tokens were read from cache, and the countdown resets to the full lifetime (60:00, or 05:00).autoWarmMaxIdle minutes; never twice in a row without a prompt from you in between.Pick a pet and it sits at the end of the HUD row on the desktop app. While Claude works, your pet types on a little laptop; between turns it does its signature move. It reacts to the session through moods, says a short word now and then as a toast, and keeps a daily streak. It is not drawn in the terminal; there, remarks arrive as toasts.
| Pet | Kind | Temperament | Signature move (idle) |
|---|---|---|---|
| Screenhead | screen bot | Earnest, literal and a little robotic; reports facts, proud of tidy output. | Types; code scrolls on its screen face. |
| Byte blob | blob | Bubbly and easily startled; squeaks with joy, wobbles when worried. | Bounces: squash, stretch and a glitch slice. |
| Pixel | cat | Dry and unimpressed, secretly proud; says little, means it. | Swishes its tail and sheds pixels. |
| Dot | ladybug | Tidy, gentle and encouraging; notices the small things. | Flutters her wings on a daisy. |
| Fork | octopus | The organised multitasker; a born lead who loves a plan. | Taps four keys with four arms. |
| Bud | dog | Eager and loyal; cheers for everything and wants to go again. | Wags, pants and flops an ear. |
Moods: idle, working, happy, tired, sleepy, worried, oops, cheering, leading, reviewing, crying. It cries when the budget cap is reached, when a daily streak of two or more days is broken, and after three failures in a row.
/glitch-pets <name | off> switches to a pet and remembers it, or empties the slot; /glitch-pets alone lists the pets. /glitch pet [name | off] works the same way.
| Setting | Default | Meaning |
|---|---|---|
pet | bud | your pet: screenhead, blob, pixel, dot, fork, bud or off; /glitch pet <name> overrides it |
voice | true | one-line remarks as toasts |
voiceEveryMinutes | 3 | at most one remark per this many minutes |
tiredMinutes | 10 | a turn this long makes the pet tired |
sleepyMinutes | 10 | this long without a prompt makes the pet sleepy |
/plugin install glitch-pets@glitch
Six built-in themes: glitch (default, electric cyan and magenta), neon, mono, sunset, matrix, paper. /glitch theme lists them, /glitch theme neon switches and remembers the choice across sessions, /glitch theme default clears it.
Resolution order: the last /glitch theme <name>, then the theme setting, then glitch. The custom setting, a JSON object of tokens, is merged over the result:
{ "accent": "#ff6ad5", "bar": "█░" }
Color tokens are accent, good, warn, alert, muted, info (as #rrggbb); bar is the full and empty characters of the context bar; icons is emoji, symbols or ascii. An unknown theme name falls back to glitch; malformed custom JSON is ignored and the chosen theme is kept. Either problem toasts at session start.
/glitch is owned by glitch-core. The HUD subcommands are answered by glitch-hud directly, the pet subcommand by glitch-pets, and the same subcommands also work as /glitch-hud ….
| Command | Owner | Does | |||
|---|---|---|---|---|---|
/glitch | core | help card: theme and the subcommands each plugin answers | |||
| `/glitch theme [name\ | default]` | core | list themes, or switch and persist | ||
| `/glitch-pets [name\ | off]` | pets | list pets, switch and persist, or turn the pet off | ||
| `/glitch pet [name\ | off]` | pets | the same, through the core command | ||
/glitch hud | hud | show or hide the band | |||
/glitch hud parts <list> | hud | parts and order for this session, as a comma list, e.g. cost,cache,context or cost, cache | |||
/glitch hud cost <list> | hud | cost figures and order for this session, e.g. session,today | |||
| `/glitch budget [+N \ | N \ | off \ | on]` | hud | show, raise, set, disable or re-enable this session's caps |
/glitch warm | hud | keep-warm ping now | |||
/glitch demo cache <min> | hud | preview the cache part and warnings at N minutes left; demo warnings say "(demo)", never auto-warm, and the real countdown comes back when the demo runs out or at the next answer | |||
/glitch demo budget <pct> | hud | preview the budget levels at N percent |
An unknown subcommand shows the help card. /glitch budget +N and N change this session's cap only; off disables both caps for this session.
Set them in /config, or in ~/.claude/settings.json under pluginConfigs. Invalid values fall back to the default and toast once.
glitch-core
| Setting | Default | Meaning |
|---|---|---|
theme | glitch | default theme: glitch, neon, mono, sunset, matrix, paper |
custom | empty | JSON object of theme tokens laid over the theme |
glitch-hud
| Setting | Default | Meaning |
|---|---|---|
parts | turn,speed,cost,cache,context,git | which parts show, in order |
costShows | reply,session,today | which cost figures show, in order |
budgetSession | 0 | USD cap per session; 0 is off |
budgetDaily | 0 | USD cap per local day, across sessions; 0 is off |
budgetBlocksAgents | false | refuse new subagents at a cap |
cacheTtlMinutes | 60 | prompt cache lifetime: 60 (most plans) or 5 |
cacheWarnMinutes | empty | minutes-left marks that notify; empty fits the lifetime (10,5 for 1 hour, 2,1 for 5 minutes); marks at or above the lifetime are ignored |
notify | windows | windows (native toast), toast (inside Claude Code) or off (no cache or cap notifications; budget toasts still show) |
autoWarm | false | keep the cache warm by itself at the last warning mark |
autoWarmMaxIdle | 120 | minutes without a prompt from you after which autoWarm stops |
/plugin install glitch-hud --marketplace <owner>/<repo>
Answer y to add the marketplace, then pick a scope. This also installs glitch-core.
From a local folder:
claude plugin marketplace add C:\path\to\this\folder
/plugin install glitch-hud@glitch
To add the pets: /plugin install glitch-pets@glitch.
Or load the plugin folders directly with CLAUDE_CODE_PLUGIN_DIRS, separated by ; on Windows. In cmd:
set CLAUDE_CODE_PLUGIN_DIRS=C:\path\to\this\folder\plugins\glitch-core;C:\path\to\this\folder\plugins\glitch-hud
In PowerShell:
$env:CLAUDE_CODE_PLUGIN_DIRS = 'C:\path\to\this\folder\plugins\glitch-core;C:\path\to\this\folder\plugins\glitch-hud'
Both last for that terminal only; to keep it, put CLAUDE_CODE_PLUGIN_DIRS under env in ~/.claude/settings.json.
Edits reach a running session with /reload-plugins.
Mods use Claude Code's early-access function-hooks API; tested on Claude Code 2.1.296.
MIT, see LICENSE.
hooks/register.tsx 87 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, PluginOptions, Register } from 'claude-code'
3
4import { THEMES, THEME_NAMES, resolveTheme } from './themes'
5
6export const theme = atom({ plugin: 'glitch-core', key: 'theme' } as const, THEMES.glitch as NonNullable<typeof THEMES.glitch>)
7
8// The `/glitch theme` choice, kept across sessions.
9export const STORE_THEME = 'theme'
10
11// Subcommands other Glitch plugins own. A command.run hook cannot run another command
12// (the host refuses it), so a routed `/glitch <sub> …` passes down the command.run chain
13// to the owner's own hook on `/glitch`; the owner must also register its `/<owner>` command,
14// which is how its presence is told.
15export const ROUTES: Record<string, string> = { hud: 'glitch-hud', budget: 'glitch-hud', warm: 'glitch-hud', demo: 'glitch-hud', pet: 'glitch-pets', crew: 'glitch-pets' }
16
17const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined)
18
19// Resolves the theme from the saved choice, else the `theme` option, and publishes it.
20export async function applyTheme($: EngineInterface, options: PluginOptions): Promise<string[]> {
21 const saved = await $.store.get(STORE_THEME).catch(() => undefined)
22 const { theme: resolved, problems } = resolveTheme(str(saved) ?? str(options.theme), str(options.custom))
23 await update($, theme, () => resolved)
24 return problems
25}
26
27async function installed($: EngineInterface): Promise<Set<string>> {
28 const list = await $.command.list().catch(() => [])
29 return new Set(list.map(c => c.name))
30}
31
32async function themeCommand($: EngineInterface, options: PluginOptions, name: string | undefined): Promise<string> {
33 const current = (await read($, theme)).name
34 if (!name) return `Themes: ${THEME_NAMES.map(n => (n === current ? `${n} (current)` : n)).join(', ')}`
35 // A store that refuses the write leaves the saved choice, so the theme stays as it is.
36 if (name === 'default') {
37 if (!(await $.store.delete(STORE_THEME).then(() => true, () => false))) return `Could not reset the theme; it stays ${current}.`
38 await applyTheme($, options)
39 return `Theme reset to ${(await read($, theme)).name}.`
40 }
41 if (!THEME_NAMES.includes(name)) return `Unknown theme "${name}". Themes: ${THEME_NAMES.join(', ')}`
42 if (!(await $.store.set(STORE_THEME, name).then(() => true, () => false))) return `Could not save the theme; it stays ${current}.`
43 await applyTheme($, options)
44 return `Theme: ${name}.`
45}
46
47async function help($: EngineInterface): Promise<string> {
48 const have = await installed($)
49 const byOwner = new Map<string, string[]>()
50 for (const [sub, owner] of Object.entries(ROUTES)) byOwner.set(owner, [...(byOwner.get(owner) ?? []), sub])
51 const lines = [`Glitch · theme ${(await read($, theme)).name}`, ' /glitch theme [name|default] — list or switch themes']
52 for (const [owner, subs] of byOwner) {
53 lines.push(` /glitch ${[...subs].sort().join(', ')} — ${owner}${have.has(owner) ? '' : ' (not installed)'}`)
54 }
55 return lines.join('\n')
56}
57
58export const register: Register = (on, options) => {
59 on('session.start', async ($, e, next) => {
60 const started = await next(e)
61 try {
62 const problems = await applyTheme($, options)
63 if (problems.length) $.ui.toast(`Glitch: ${problems.join('; ')}`, { timeoutMs: 8000 })
64 } catch {
65 $.ui.toast('Glitch: could not apply theme', { timeoutMs: 8000 })
66 }
67 try {
68 await $.command.register({ name: 'glitch', description: 'Glitch kit: themes, HUD, budget, cache keep-warm, pets (/glitch for help)' })
69 } catch {
70 $.ui.toast('Glitch: could not register /glitch', { timeoutMs: 8000 })
71 }
72 return started
73 })
74
75 on('command.run', { command: 'glitch' }, async ($, e, next) => {
76 const [sub = '', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
77 if (sub === 'theme') return { text: await themeCommand($, options, rest[0]) }
78
79 const owner = Object.hasOwn(ROUTES, sub) ? ROUTES[sub] : undefined
80 if (owner) {
81 if (!(await installed($)).has(owner)) return { text: `/glitch ${sub} needs the ${owner} plugin, which is not installed.` }
82 return next(e)
83 }
84 return { text: await help($) }
85 })
86}
87hooks/themes.ts 63 lines1import type { Theme } from '../types'
2
3export const THEMES: Record<string, Theme> = {
4 glitch: { name: 'glitch', accent: '#00e5ff', good: '#5af78e', warn: '#f3c969', alert: '#ff5c8a', muted: '#7a7f8c', info: '#c77dff', bar: ['▓', '░'], icons: 'symbols' },
5 neon: { name: 'neon', accent: '#ff2bd6', good: '#39ff14', warn: '#ffd300', alert: '#ff3131', muted: '#8a8aa8', info: '#00f0ff', bar: ['█', '▁'], icons: 'symbols' },
6 mono: { name: 'mono', accent: '#ffffff', good: '#d0d0d0', warn: '#b0b0b0', alert: '#ffffff', muted: '#808080', info: '#c0c0c0', bar: ['■', '□'], icons: 'ascii' },
7 sunset: { name: 'sunset', accent: '#ff8c42', good: '#a3d977', warn: '#ffc857', alert: '#e84855', muted: '#9c8f8a', info: '#c06cde', bar: ['█', '░'], icons: 'emoji' },
8 matrix: { name: 'matrix', accent: '#00ff41', good: '#00ff41', warn: '#c6ff00', alert: '#ff073a', muted: '#008f11', info: '#00d1a0', bar: ['█', '·'], icons: 'symbols' },
9 paper: { name: 'paper', accent: '#3b5bdb', good: '#2b8a3e', warn: '#e67700', alert: '#c92a2a', muted: '#868e96', info: '#7048e8', bar: ['▓', '░'], icons: 'symbols' },
10}
11
12export const THEME_NAMES = Object.keys(THEMES)
13export const DEFAULT_THEME = 'glitch'
14
15const COLOR = /^#[0-9a-fA-F]{6}$/
16const COLOR_TOKENS = ['accent', 'good', 'warn', 'alert', 'muted', 'info'] as const
17
18const copy = (t: Theme): Theme => ({ ...t, bar: [t.bar[0], t.bar[1]] })
19
20// A built-in theme with the `custom` JSON's tokens laid over it. Anything that
21// does not check out is skipped and named in `problems`; nothing throws.
22export function resolveTheme(name: string | undefined, custom: string | undefined): { theme: Theme; problems: string[] } {
23 const problems: string[] = []
24 let base = THEMES[DEFAULT_THEME] as Theme
25 if (name) {
26 const known = Object.hasOwn(THEMES, name) ? THEMES[name] : undefined
27 if (known) base = known
28 else problems.push(`unknown theme "${name}", using ${DEFAULT_THEME}`)
29 }
30 const theme = copy(base)
31 if (!custom || !custom.trim()) return { theme, problems }
32
33 let parsed: unknown
34 try {
35 parsed = JSON.parse(custom)
36 } catch {
37 problems.push('custom theme is not valid JSON, ignored')
38 return { theme, problems }
39 }
40 if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
41 problems.push('custom theme must be a JSON object, ignored')
42 return { theme, problems }
43 }
44
45 const tokens = parsed as Record<string, unknown>
46 for (const key of COLOR_TOKENS) {
47 const value = tokens[key]
48 if (value === undefined) continue
49 if (typeof value === 'string' && COLOR.test(value)) theme[key] = value
50 else problems.push(`custom.${key} must be a #rrggbb color`)
51 }
52 if (tokens.bar !== undefined) {
53 const chars = typeof tokens.bar === 'string' ? [...tokens.bar] : []
54 if (chars.length === 2) theme.bar = [chars[0] as string, chars[1] as string]
55 else problems.push('custom.bar must be exactly two characters')
56 }
57 if (tokens.icons !== undefined) {
58 if (tokens.icons === 'emoji' || tokens.icons === 'symbols' || tokens.icons === 'ascii') theme.icons = tokens.icons
59 else problems.push('custom.icons must be emoji, symbols or ascii')
60 }
61 return { theme, problems }
62}
63types/index.d.ts 21 lines1export type ThemeIcons = 'emoji' | 'symbols' | 'ascii'
2
3export type Theme = {
4 name: string
5 accent: string
6 good: string
7 warn: string
8 alert: string
9 muted: string
10 info: string
11 /** Progress-bar characters: [filled, empty]. */
12 bar: [string, string]
13 icons: ThemeIcons
14}
15
16declare module 'claude-code' {
17 interface PluginState {
18 'glitch-core': { theme: Theme }
19 }
20}
21