A folder-tree card beside the status bar listing your projects by category, read from ~/.claude/project-box.json.


Claude Code now has mods: plugins made of TypeScript function hooks that can draw UI right inside the terminal, react to everything Claude does, and change how it works. This repo contains four mods I use every day. There's a pixel critter that lives above your prompt, a control panel for model, effort and parallel agents, a project launcher box, and a live status bar. It also has everything you need to build your own in about 10 minutes.
⚠️ Mods are early access. The function-hooks API can change between Claude Code releases. Everything here was built and tested on Claude Code 2.1.294.
Open Claude Code in a terminal and paste:
/plugin install pixel-buddy --marketplace Bocskay-Botond/claude-code-mods
Answer y to add the marketplace, press Enter for the user scope, and you're done. A little orange critter appears above your prompt. Start typing and watch it pull out a laptop. 🧡
Want the full setup? 👉 quick-start/
| Mod | What it does | Commands |
|---|---|---|
| 🐾 pixel-buddy | A 12×4 pixel Claude critter above the prompt. It types on a tiny laptop while you type, patrols while Claude works, celebrates when a turn finishes, plays (keepy-uppy, jump rope, butterflies, bubbles, reading) when you're idle, and falls asleep after 3 minutes. | /buddy /pet |
| 🛠️ tristens-tools | A tools menu and status line: model & effort picker, Clean View (hides tool noise and shows a simple checklist), Prompt Upgrade, Agent Dock (split each request across 1–100 helper agents and watch them live), and a Roadtrip racing minigame. | /tools /dock /roadtrip |
| 📁 project-box | A folder-tree card beside the status bar listing your projects by category. Click a category, then a project, to see its note and path. pixel-buddy walks over and "types the cd" for you. | /projects |
| 📊 statusbar | A minimal one-line status bar: working/idle, tool count, last tool, last turn duration, turns. The simplest real mod in the repo, so read this one first. | — |
| 👋 hello-mod | The tutorial example: a slash command, a toast and a band in about 35 lines. | /hello |
Install any of them the same way:
/plugin install <mod-name> --marketplace Bocskay-Botond/claude-code-mods
claude-code-mods/
├── .claude-plugin/
│ └── marketplace.json # makes this repo a plugin marketplace
├── mods/
│ ├── pixel-buddy/ # 🐾 the critter
│ ├── tristens-tools/ # 🛠️ tools menu, Agent Dock, Roadtrip
│ ├── project-box/ # 📁 project launcher card
│ └── statusbar/ # 📊 one-line status bar
├── examples/
│ └── hello-mod/ # 👋 your first mod, fully commented
├── quick-start/ # ⚡ 5-minute setup
├── docs/
│ ├── installation.md # every way to install & manage mods
│ ├── writing-your-own-mod.md
│ ├── api-cheatsheet.md # events, $ nouns, UI elements
│ └── troubleshooting.md
├── mod-builder-prompt/ # 🤖 a prompt that makes Claude build mods for you
└── assets/ # 🖼️ social preview (HTML source + PNG)
Every mod is the same four small files:
my-mod/
├── .claude-plugin/plugin.json # name, version, description
├── hooks/hooks.json # { "modules": ["./register.tsx"] }
├── hooks/register.tsx # the code
└── types/index.d.ts # the state your mod keeps (optional)
on(event, hook) / next(e) middleware chain worksclaude plugin test and ship it as a GitHub marketplaceclaude --version)No Node, no npm install, no build step. Claude Code compiles the TypeScript itself.
The fastest way is to ask Claude. Inside Claude Code:
Make me a mod that shows a 🍅 pomodoro timer above the prompt and plays a toast when it ends.
Claude Code has a built-in skill for this: it writes the mod, hot-reloads it into your session and tests it. For a better result, paste the prompt from mod-builder-prompt/ first.
Prefer to do it by hand? docs/writing-your-own-mod.md walks through hello-mod line by line, and docs/api-cheatsheet.md has the events and elements you'll use most.
project-box shows a demo list until you give it yours. Copy the example and edit it:
# macOS / Linux
cp mods/project-box/project-box.example.json ~/.claude/project-box.json
# Windows (PowerShell)
Copy-Item mods\project-box\project-box.example.json $HOME\.claude\project-box.json
Restart Claude Code (or run /reload-plugins) and your projects show up. Colors can be any terminal color name (magenta, cyan, green, yellow, blue, red). Set "live": true for a green dot.
| Problem | Fix |
|---|---|
| Nothing appears after install | Run /reload-plugins, or restart Claude Code. Make sure you're in a terminal: /plugin isn't available in the desktop app's Code tab. |
Marketplace file not found | Check the repo name: Bocskay-Botond/claude-code-mods |
| A mod silently does nothing | Start with claude --debug. A failing hook is skipped and logged with the reason. |
| Two bands fight for space | pixel-buddy, project-box and tristens-tools are designed to stack. Turn things off with /buddy, /projects, or /plugin → disable. |
More: docs/troubleshooting.md
Built a cool mod? PRs are welcome!
mods/<your-mod>/ (four files, see above)claude plugin validate mods/<your-mod> and claude plugin test mods/<your-mod> pass.claude-plugin/marketplace.json and to the table in this READMEMIT. Do whatever you want, just don't blame me if your critter falls asleep on the job. 😴
tristens-tools (Clean View, Agent Dock, Roadtrip and the tools menu). Go watch it!If this made your terminal a little more fun, ⭐ star the repo and share it with someone who lives in Claude Code.
Now go build a mod. Your terminal has been boring for long enough. 🚀
hooks/register.tsx 148 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Category } from '../types'
5
6// Shown until ~/.claude/project-box.json exists (see project-box.example.json).
7const DEMO: Category[] = [
8 {
9 id: 'ai', name: 'AI & Agents', color: 'magenta',
10 projects: [
11 { id: 'assistant', name: 'Home Assistant', dir: 'projects/assistant', note: 'Multi-agent assistant with a FastAPI dashboard' },
12 { id: 'voice', name: 'Voice Wake-up', dir: 'projects/voice', note: 'Wake word -> STT -> LLM -> TTS' },
13 ],
14 },
15 {
16 id: 'web', name: 'Websites', color: 'green',
17 projects: [
18 { id: 'booking', name: 'Booking App', dir: 'projects/booking', note: 'Node/Express/SQLite appointment booking, live at example.com', live: true },
19 { id: 'landing', name: 'Landing Page', dir: 'projects/landing', note: 'GSAP-animated landing page' },
20 ],
21 },
22 {
23 id: 'auto', name: 'Automation', color: 'yellow',
24 projects: [
25 { id: 'news', name: 'Daily News', dir: 'scripts', note: 'Scheduled morning news email', live: true },
26 { id: 'mods', name: 'Claude mods', dir: 'claude-code-mods', note: 'This repo: buddy, tools, this box' },
27 ],
28 },
29]
30
31const WIDE = 46
32
33const isHidden = atom({ plugin: 'project-box', key: 'isHidden' } as const, false)
34const openCat = atom({ plugin: 'project-box', key: 'openCat' } as const, '')
35const openProject = atom({ plugin: 'project-box', key: 'openProject' } as const, '')
36const categories = atom({ plugin: 'project-box', key: 'categories' } as const, DEMO)
37
38const isCategory = (c: unknown): c is Category => {
39 const x = c as Category
40 return typeof x?.id === 'string' && typeof x.name === 'string' && Array.isArray(x.projects)
41 && x.projects.every(p => typeof p?.id === 'string' && typeof p.name === 'string')
42}
43
44const loadConfig = async ($: EngineInterface): Promise<Category[]> => {
45 const home = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME'))
46 if (home === undefined) return DEMO
47 const text = await $.fs.read(`${home}/.claude/project-box.json`).catch(() => undefined)
48 if (text === undefined) return DEMO
49 try {
50 const list = (JSON.parse(text) as { categories?: unknown }).categories
51 if (!Array.isArray(list) || !list.every(isCategory)) throw new Error('bad shape')
52 return list.map(c => ({
53 ...c,
54 color: c.color ?? 'cyan',
55 projects: c.projects.map(p => ({ ...p, dir: p.dir ?? '', note: p.note ?? '' })),
56 }))
57 } catch {
58 $.ui.toast('project-box: ~/.claude/project-box.json is not valid, showing the demo list')
59 return DEMO
60 }
61}
62
63export const register: Register = on => {
64 on('session.start', async ($, e, next) => {
65 await $.command.register({ name: 'projects', description: 'Show or hide the project box' })
66 const list = await loadConfig($)
67 await update($, categories, () => list)
68
69 return next(e)
70 })
71
72 on('command.run', { command: 'projects' }, async $ => {
73 const hidden = await update($, isHidden, h => !h)
74
75 return { text: hidden ? 'Project box hidden.' : 'Project box shown.' }
76 })
77
78 // The card sits inside the band, to the right of the status rows.
79 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
80 const below = await next(e)
81 if (e.props.hasSurvey || (await read($, isHidden))) return below
82
83 const { Box, Text, Button } = $.ui.resolve(e)
84 const [cats, cat, proj] = await Promise.all([read($, categories), read($, openCat), read($, openProject)])
85 const total = cats.reduce((n, c) => n + c.projects.length, 0)
86 // pixel-buddy walks up to a closed card 24 columns wide
87 const narrow = Math.min(30, Math.max(24, ...cats.map(c => c.name.length + 8)))
88 const width = cat === '' ? narrow : Math.min(WIDE, Math.max(narrow, e.props.bodyColumns - 40))
89
90 const toggleCat = async (id: string) => {
91 await update($, openCat, cur => (cur === id ? '' : id))
92 await update($, openProject, () => '')
93 }
94 const toggleProject = (id: string) => update($, openProject, cur => (cur === id ? '' : id))
95
96 return (
97 <Box flexDirection="row" alignItems="flex-end">
98 <Box flexDirection="column" flexGrow={1} flexShrink={1}>
99 {below}
100 </Box>
101 <Box width={width} flexShrink={0} marginLeft={2} flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
102 <Box justifyContent="space-between">
103 <Text>
104 <Text bold>◆ Projects</Text>
105 <Text dimColor> {total}</Text>
106 </Text>
107 {cat !== '' && <Button key="close" plain dimColor label="✕" onPress={() => toggleCat(cat)} />}
108 </Box>
109 {cats.map((c, i) => {
110 const isOpen = c.id === cat
111 return (
112 <Box flexDirection="column">
113 <Box justifyContent="space-between">
114 <Box>
115 <Text color={c.color}>{isOpen ? '▾ ' : '▸ '}</Text>
116 <Button key={`cat:${c.id}`} plain hotkey={i < 9 ? String(i + 1) : undefined} label={c.name} onPress={() => toggleCat(c.id)} />
117 </Box>
118 <Text dimColor>{c.projects.length}</Text>
119 </Box>
120 {isOpen &&
121 c.projects.map((p, j) => {
122 const isLast = j === c.projects.length - 1
123 const isSel = p.id === proj
124 return (
125 <Box flexDirection="column">
126 <Box>
127 <Text dimColor>{isLast ? ' └ ' : ' ├ '}</Text>
128 <Text color={p.live ? 'green' : 'gray'}>{p.live ? '● ' : '◇ '}</Text>
129 <Button key={`p:${p.id}`} plain label={p.name} onPress={() => toggleProject(p.id)} />
130 </Box>
131 {isSel && (
132 <Box flexDirection="column" paddingLeft={5}>
133 <Text wrap="wrap">{p.note}</Text>
134 <Text dimColor wrap="truncate-middle">{p.dir === '' ? '~' : `~/${p.dir}`}</Text>
135 </Box>
136 )}
137 </Box>
138 )
139 })}
140 </Box>
141 )
142 })}
143 </Box>
144 </Box>
145 )
146 })
147}
148types/index.d.ts 16 lines1export type Flag = boolean
2export type Id = string
3export type Project = { id: string; name: string; dir: string; note: string; live?: boolean }
4export type Category = { id: string; name: string; color: string; projects: Project[] }
5
6declare module 'claude-code' {
7 interface PluginState {
8 'project-box': {
9 isHidden: Flag
10 openCat: Id
11 openProject: Id
12 categories: Category[]
13 }
14 }
15}
16