A lunchbox that fills as context grows, with /doggybag to pack a handoff for a fresh session.

The mods I run in Claude Code, each in its own folder. Clone this repo and run one script to get the same setup on any machine.
Mods are small plugins that add live panes, status bands and slash commands to Claude Code. They hot-reload when you edit them.
git clone https://github.com/wpvip-awesomeface/claude-mods.git ~/.claude/mods
~/.claude/mods/install.sh
Then start a new Claude Code session. Add --all to also turn on the personal mods in fun/ (that's the full backup restore).
The script:
CLAUDE_CODE_PLUGIN_DIRS in ~/.claude/settings.json)CLAUDE_CODE_PLUGIN_DIR_WATCH=1)mods command into ~/.local/binmods # list mods and whether they're on
mods on bento # load one in every new session
mods off workbench # stop loading it
mods on all # the core mods
mods on fun # the personal mods in fun/
| Mod | What you see | Saves tokens? |
|---|---|---|
| bento | A band above the prompt showing context use, turns and subagents | Yes, directly |
| turn-timer | "Speed Radar": how long each reply took | Indirectly |
| recipe-card | A card under each answer showing the route line, with /recipe to save a solve | Yes, over time |
workbench (in fun/) | A side pane with your open PRs, next meeting, pizza and a dino | No, it's for focus and fun |
What: a lunchbox band above the prompt. The box fills as context grows and shows context %, tokens, turn count and how many subagents ("helpers") the session has started.
Why: cost is roughly turns × context size, and both are invisible by default. Bento makes them visible, and warns you once at 70% context or 60 turns that it's time to start fresh.
Commands:
/doggybag or /handoff: writes a 5–10 line handoff (goal, state, links, next step) and copies it to your clipboard. It runs as one tool-less fork over the cached transcript, so it's cheap. Paste it into a new session./hush: hides every mod at once, for screenshares and recordings./bento: shows or hides the band.What: shows how long each reply took (Speed Radar: ⏱ 2m 15s), drawn above the route line.
Why: slow replies are usually long, tool-heavy turns. Seeing the time makes it easy to spot when a session is dragging and should be split up. It stays visible during /hush, since it's safe to show in demos.
What: reads the route line at the end of each answer (◎ shape · † graveyard · ◫ library · ≡ rung · ✦ extract) and draws it as a card. When an answer was a fresh solve worth keeping, it offers /recipe to save it as a reusable tool.
Why: the cheapest task is the one you've already solved. Saving solves to a library, and dead ends to a graveyard, keeps future sessions from paying to re-solve them.
Needs: a skills library that uses the route line. Without one, the card has nothing to read.
Heads up: this is a personal mod, built around the author's own workflow (their demo sites, calendar and sense of humor). Most people won't need or want it. It's here so the author's full setup is backed up, and as an example if you want to build your own.
It lives in fun/, and the installer leaves it off unless you pass --all or run mods on workbench.
What: a side pane with:
/prep to get ready for it/fun, off by default): a quote of the day, pizza and mochi the dinoWhy: keeps PR status and your calendar in view, so you don't spend turns asking Claude to check them.
Needs: the gh CLI signed in, and a Google Calendar connector for birdy. The demo links and the company email domain are set at the top of fun/workbench/hooks/register.tsx, so change those for your own setup. Edit the quote list with /quotes.
Commands: /workbench, /greg-check, /prep, /quotes, /fun.
Not a mod, but it lives here too: token-kit/ has the CLAUDE.md rules and a read guard hook that blocks whole-file reads over 400 lines once and returns an outline. To have an agent set all of it up, point it at AGENT_SETUP.md.
<mod>/hooks/register.tsx.*/.claude-plugin/types/ is generated by Claude Code on each machine and is not committed. Your editor may show type errors until a session has loaded the mods once.hooks/register.tsx 217 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface as Dollar, Register } from 'claude-code'
3
4import type { Lunch } from '../types'
5
6const EMPTY: Lunch = { startedAt: 0, turns: 0, helpers: 0, percent: 0, tokens: 0 }
7
8const lunch = atom({ plugin: 'bento', key: 'lunch' } as const, EMPTY)
9const warned = atom({ plugin: 'bento', key: 'warned' } as const, { context: false, turns: false })
10const isHidden = atom({ plugin: 'bento', key: 'isHidden' } as const, false)
11// The master switch: /hush flips it, and the other mods follow it (they hook its writes).
12const hushed = atom({ plugin: 'bento', key: 'hushed' } as const, false)
13// A colored notice in bento's mint, in place of plain toasts.
14const flash = atom({ plugin: 'bento', key: 'flash' } as const, null)
15
16const CONTEXT_LIMIT = 70
17const TURN_LIMIT = 60
18
19const HANDOFF_PROMPT = [
20 'Pack a doggy bag: write a 5-10 line handoff for this session so I can start fresh.',
21 'Cover goal, current state, links (PRs, preview URLs, files), and the next step.',
22 'Put it in one fenced block I can paste into a new session. Keep it tight.',
23].join(' ')
24
25// The handoff as the command's own output: one tool-less fork over this
26// session's transcript (served from the prompt cache), so it works on demand
27// and never waits on a queued turn. Copied to the clipboard as well.
28const FORK_PROMPT = [
29 HANDOFF_PROMPT,
30 'Reply with only the fenced block: no preamble, no follow-up questions, no tool use.',
31].join(' ')
32
33const unfence = (text: string) => {
34 const m = text.match(/```[a-z]*\n([\s\S]*?)```/)
35 return (m?.[1] ?? text).trim()
36}
37
38const pack = async ($: Dollar): Promise<string> => {
39 const reply = await $.model.fork({ prompt: FORK_PROMPT })
40 if (!reply.isAnswered) {
41 if (reply.reason === 'nothing-to-fork') {
42 return 'Nothing to pack yet: this session has no replies to hand off.'
43 }
44 // Fall back to asking in a normal turn.
45 void $.prompt.submit({ text: HANDOFF_PROMPT })
46 return `Couldn't pack it directly (${reply.reason}); asked in the next turn instead.`
47 }
48 const handoff = unfence(reply.text)
49 const copied = await $.ui.copy({ text: handoff })
50 const note = copied.isCopied ? 'Copied to your clipboard. ' : ''
51 return `Doggy bag packed. ${note}Paste it into a new session:\n\n\`\`\`\n${handoff}\n\`\`\``
52}
53
54// Quiet palette: green for the name only, greys for everything else.
55const SAGE = '#6CF5A8' // the name: bright Dracula-style mint
56const TEXT = '#D4D4D4' // words
57const DATA = '#A3A3A3' // numbers, a step darker so they stand apart
58
59// Single-color glyphs only: the plate fills as context grows.
60const plate = (percent: number) =>
61 percent < 25 ? { icon: '○', mood: 'plenty of room' }
62 : percent < 50 ? { icon: '◔', mood: 'nibbling' }
63 : percent < CONTEXT_LIMIT ? { icon: '◑', mood: 'half full' }
64 : percent < 85 ? { icon: '◕', mood: 'getting full, /handoff soon' }
65 : { icon: '●', mood: 'stuffed, /handoff now' }
66
67const FLASH_MS = 10_000
68
69const say = async ($: Dollar, text: string) => {
70 const id = await $.clock.now()
71 await update($, flash, () => ({ glyph: '▦', name: 'bento', color: SAGE, text, id }))
72 $.clock.after(FLASH_MS, () => void update($, flash, f => (f && f.id === id ? null : f)))
73}
74
75const short = (n: number) => (n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`)
76
77export const register: Register = on => {
78 on('session.start', async ($, e, next) => {
79 await $.command.register({
80 name: 'doggybag',
81 description: 'Pack a short handoff so you can start a fresh session',
82 })
83 await $.command.register({
84 name: 'handoff',
85 description: 'Write a short handoff for a fresh session (same as /doggybag)',
86 })
87 await $.command.register({
88 name: 'hush',
89 description: 'Hide or show every mod at once (bento, Workbench, recipe card), e.g. for screenshares',
90 })
91 await $.command.register({
92 name: 'bento',
93 description: 'Show or hide the bento band above the prompt',
94 })
95
96 return next(e)
97 })
98
99 // /handoff is the plain-named alias of /doggybag.
100 for (const command of ['doggybag', 'handoff']) {
101 on('command.run', { command }, async $ => {
102 await say($, 'Packing a doggy bag…')
103 return { text: await pack($) }
104 })
105 }
106
107 on('command.run', { command: 'hush' }, async $ => {
108 const isHushed = await update($, hushed, was => !was)
109 await update($, isHidden, () => isHushed)
110
111 return { text: isHushed ? 'Hushed: mods hidden. /hush brings them back.' : 'Mods are back.' }
112 })
113
114 on('command.run', { command: 'bento' }, async $ => {
115 const hidden = await update($, isHidden, was => !was)
116
117 return { text: hidden ? 'Bento tucked away.' : 'Bento is back.' }
118 })
119
120 // Count helpers the main loop sends out (subagents).
121 on('tool.call', async ($, e, next) => {
122 if (e.tool === 'Agent' && !e.agentId) {
123 await update($, lunch, l => ({ ...l, helpers: l.helpers + 1 }))
124 }
125
126 return next(e)
127 }).catch(($, e, next) => next(e)) // a failed count never blocks the tool call
128
129 on('turn.complete', async ($, e, next) => {
130 const result = await next(e)
131
132 if (e.agentId) {
133 return result
134 }
135
136 const usage = await $.session.usage()
137 const now = await update($, lunch, l => {
138 // A new startedAt means /clear or a new session: start a fresh box.
139 const base = l.startedAt === usage.startedAt ? l : { ...EMPTY, startedAt: usage.startedAt }
140
141 return {
142 ...base,
143 turns: base.turns + 1,
144 percent: usage.context.percent ?? base.percent,
145 tokens: usage.context.tokens ?? base.tokens,
146 }
147 })
148
149 if (now.turns === 1) {
150 await update($, warned, () => ({ context: false, turns: false }))
151 }
152
153 const was = await read($, warned)
154
155 if (!was.context && now.percent >= CONTEXT_LIMIT) {
156 await update($, warned, w => ({ ...w, context: true }))
157 await say($, `${now.percent}% full. Run /handoff and start fresh.`)
158 } else if (!was.turns && now.turns >= TURN_LIMIT) {
159 await update($, warned, w => ({ ...w, turns: true }))
160 await say($, `${now.turns} turns in this box. Run /handoff and start fresh.`)
161 }
162
163 return result
164 })
165
166 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
167 const l = await read($, lunch)
168 const f = await read($, flash)
169 const { Box, Text, Button } = $.ui.resolve(e)
170 const notice = !f || e.props.hasSurvey ? null : (
171 <Box gap={1}>
172 <Text><Text bold color={f.color}>{f.glyph} {f.name} </Text><Text color={TEXT}>{f.text}</Text></Text>
173 <Button key="flash-close" plain dimColor label="×" onPress={() => update($, flash, () => null)} />
174 </Box>
175 )
176
177 if (e.props.hasSurvey || l.turns === 0 || (await read($, isHidden))) {
178 if (!notice) return next(e)
179 const rest = await next(e)
180 return <Box flexDirection="column">{notice}{rest}</Box>
181 }
182
183 const { icon, mood } = plate(l.percent)
184 const isFull = l.percent >= CONTEXT_LIMIT || l.turns >= TURN_LIMIT
185 const dot = <Text color={DATA}> · </Text>
186 // Draw whatever sits beneath (other mods' bands) under this one, so bands stack.
187 const below = await next(e)
188
189 return (
190 <Box flexDirection="column">
191 {notice}
192 <Box gap={1}>
193 <Text>
194 <Text bold color={SAGE}>▦ bento </Text>
195 <Text color={TEXT}>{icon} </Text>
196 <Text color={DATA}>{l.percent}%</Text>
197 {dot}
198 <Text color={DATA}>{short(l.tokens)}</Text>
199 {dot}
200 <Text color={TEXT}>turn </Text>
201 <Text color={DATA}>{l.turns}</Text>
202 {l.helpers > 0 && dot}
203 {l.helpers > 0 && <Text color={DATA}>{l.helpers}</Text>}
204 {l.helpers > 0 && <Text color={TEXT}> helper{l.helpers === 1 ? '' : 's'}</Text>}
205 {dot}
206 <Text color={TEXT}>{mood}</Text>
207 </Text>
208 {isFull && (
209 <Button key="bag" label="Doggy bag" onPress={() => void $.command.run({ command: 'handoff', args: '' })} />
210 )}
211 </Box>
212 {below}
213 </Box>
214 )
215 })
216}
217types/index.d.ts 18 lines1export type Lunch = {
2 startedAt: number
3 turns: number
4 helpers: number
5 percent: number
6 tokens: number
7}
8
9export type Flash = { glyph: string; name: string; color: string; text: string; id: number }
10
11export type Warned = { context: boolean; turns: boolean }
12
13declare module 'claude-code' {
14 interface PluginState {
15 bento: { lunch: Lunch; warned: Warned; isHidden: boolean; hushed: boolean; flash: Flash | null }
16 }
17}
18