SLOPSHOPPER

glitch-core

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

newcommandtoast
v0.2.0MITupdated 2026-10-14Sri-Ln/glitch/plugins/glitch-core
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · glitch-core
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /glitch ⎿ glitch-core: Glitch · theme glitch ⎿ glitch-core: /glitch theme [name|default] — list or switch themes ⎿ glitch-core: /glitch budget, demo, hud, warm — glitch-hud (not installed) ⎿ glitch-core: /glitch crew, pet — glitch-pets (not installed) ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Glitch

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.

PluginWhat it does
glitch-coreShared themes and the /glitch command.
glitch-hudThe band above the prompt: turn, speed, cost and budget, cache countdown with keep-warm, context, git. Depends on glitch-core.
glitch-petsA pixel pet at the end of the HUD row, with moods, short remarks and a daily streak. Depends on glitch-core and glitch-hud.

The HUD

One line above the prompt. In the terminal it is text with theme colors and icons:

PartExampleSource
turn#12count of main-session turns
speed⚡ 84 tok/soutput 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 todaythe 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 ↑1branch, 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.

Budget guard

Set a cap per session (budgetSession), per local day (budgetDaily), or both. 0 turns a cap off.

  • 75% of a cap: the cost turns amber.
  • 90%: red, plus a toast.
  • 100%: toast plus a notification.

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.

Cache keeper

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).

  • Warnings. At each warning mark, once per answer, while idle, you get a notification titled <project> — Glitch: "Prompt cache expires in N min".
  • Notification chain. With 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).
  • Keep-warm. ↻ 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).
  • Cost. One ping is a cache read of the whole context: about $0.04 at 200k tokens of context on Opus 5.5, versus about $1.60 to rebuild an expired cache.
  • autoWarm (off by default). At the last warning mark, ping once if you submitted a prompt within autoWarmMaxIdle minutes; never twice in a row without a prompt from you in between.

glitch-pets

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.

PetKindTemperamentSignature move (idle)
Screenheadscreen botEarnest, literal and a little robotic; reports facts, proud of tidy output.Types; code scrolls on its screen face.
Byte blobblobBubbly and easily startled; squeaks with joy, wobbles when worried.Bounces: squash, stretch and a glitch slice.
PixelcatDry and unimpressed, secretly proud; says little, means it.Swishes its tail and sheds pixels.
DotladybugTidy, gentle and encouraging; notices the small things.Flutters her wings on a daisy.
ForkoctopusThe organised multitasker; a born lead who loves a plan.Taps four keys with four arms.
BuddogEager 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.

SettingDefaultMeaning
petbudyour pet: screenhead, blob, pixel, dot, fork, bud or off; /glitch pet <name> overrides it
voicetrueone-line remarks as toasts
voiceEveryMinutes3at most one remark per this many minutes
tiredMinutes10a turn this long makes the pet tired
sleepyMinutes10this long without a prompt makes the pet sleepy
/plugin install glitch-pets@glitch

Themes

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.

Commands

/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 ….

CommandOwnerDoes
/glitchcorehelp card: theme and the subcommands each plugin answers
`/glitch theme [name\default]`corelist themes, or switch and persist
`/glitch-pets [name\off]`petslist pets, switch and persist, or turn the pet off
`/glitch pet [name\off]`petsthe same, through the core command
/glitch hudhudshow or hide the band
/glitch hud parts <list>hudparts and order for this session, as a comma list, e.g. cost,cache,context or cost, cache
/glitch hud cost <list>hudcost figures and order for this session, e.g. session,today
`/glitch budget [+N \N \off \on]`hudshow, raise, set, disable or re-enable this session's caps
/glitch warmhudkeep-warm ping now
/glitch demo cache <min>hudpreview 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>hudpreview 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.

Settings

Set them in /config, or in ~/.claude/settings.json under pluginConfigs. Invalid values fall back to the default and toast once.

glitch-core

SettingDefaultMeaning
themeglitchdefault theme: glitch, neon, mono, sunset, matrix, paper
customemptyJSON object of theme tokens laid over the theme

glitch-hud

SettingDefaultMeaning
partsturn,speed,cost,cache,context,gitwhich parts show, in order
costShowsreply,session,todaywhich cost figures show, in order
budgetSession0USD cap per session; 0 is off
budgetDaily0USD cap per local day, across sessions; 0 is off
budgetBlocksAgentsfalserefuse new subagents at a cap
cacheTtlMinutes60prompt cache lifetime: 60 (most plans) or 5
cacheWarnMinutesemptyminutes-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
notifywindowswindows (native toast), toast (inside Claude Code) or off (no cache or cap notifications; budget toasts still show)
autoWarmfalsekeep the cache warm by itself at the last warning mark
autoWarmMaxIdle120minutes without a prompt from you after which autoWarm stops

Install

/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.

Notes

Mods use Claude Code's early-access function-hooks API; tested on Claude Code 2.1.296.

License

MIT, see LICENSE.

Source 3 files
hooks/register.tsx 87 lines
1import { 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}
87
hooks/themes.ts 63 lines
1import 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}
63
types/index.d.ts 21 lines
1export 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