A receipt row above the prompt after each turn: time, tools, files edited, cost

Mods for Claude Code: small plugins that add to its screen and behavior. Each folder is one mod. Turn on the ones you want (see Setup) and change their settings in /config, under the mod's name.
Shows how full Claude's context window is, as a colored bar above the prompt.
/context-bar hides or shows it for this session.Shows how much of your 5-hour and weekly usage limits you've used, as two small meters under the context bar. They turn yellow at 70% and red at 90%, and you get a warning once a limit passes 90%.
Shows this session's ID in the bottom-right corner of the screen.
claude --resume <id> command.Adds a receipt to the line that closes each turn ("✻ Baked for 42s"): how many tools ran, which files were edited, the tokens used and what the turn cost.
Adds a one-sentence summary under Claude's longer replies, written by Claude Haiku.
/config sets how long a reply must be (600 characters by default).Lets you see images Claude reads, right in the conversation.
🖼 name.png · hover to preview. Point at it to show the picture, and move away to hide it.Gives Claude a voice, through murmur. It speaks only when you're away from the session (another app or tab in front), so it never talks over you while you're watching.
/read reads Claude's last reply aloud./tldr-aloud gives a spoken 2–3 sentence summary of the session so far./config./hush mutes this session; run it again to unmute./config under voice.Ties Claude into the Ghostty terminal. All its settings start with "Ghostty" in /config.
/title gets a new one right away. A resumed session gets its old title back./workspace saves and reopens whole windows of sessions: see Workspaces./goto (or ⌘⌃G) lists your open Claude sessions, the ones waiting on you first, and jumps to the one you pick. /goto next jumps straight to the one waiting longest; /goto <words> jumps to the one whose title matches./keybind add <keys> <text> makes a Ghostty key type something into Claude, like /keybind add super+ctrl+h /hush for ⌘⌃H. /keybind lists them, and /keybind remove <keys> removes one.A workspace is a Ghostty window of Claude sessions you can put away and bring back later. It remembers every tab and split: each Claude session with its conversation, and each shell with its folder. Reopening it brings back the whole window, with every conversation picking up where it left off.
| Command | What it does |
|---|---|
/workspace save [name] | Saves this window. With no name, it keeps the name the window already had, or Claude Haiku picks one from the tabs. |
/workspace close [name] | Saves this window, then exits every Claude session in it and closes their tabs. Your current tab stays open at the shell, and shell tabs stay open. |
/workspace list | Shows your saved workspaces, newest first. Press a number or Enter to reopen one, or Tab to ✕ delete to remove it. |
/workspace open <name> | Reopens a saved workspace in a new window. Part of the name is enough. |
claude --resume, so they continue their own conversation, with the tab title they had when you saved. A tab saved without a title gets a new one from its conversation. Shell tabs open at the prompt./workspace close. Pick it up tomorrow: /workspace list, then press its number.~/.claude/workspaces/, one JSON file per workspace.CLAUDE_CODE_PLUGIN_DIRS, separated by :, in the env block of ~/.claude/settings.json: "env": {
"CLAUDE_CODE_PLUGIN_DIRS": "/path/to/claude-mods/context-bar:/path/to/claude-mods/voice"
}
To try one mod for a single session: claude --plugin-dir /path/to/claude-mods/tldr.
CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 in the same env block, so Claude Code doesn't overwrite the tab title.The first time a mod controls Ghostty, macOS asks for permission. Allow it.
Editing a mod's files reloads it in running sessions.
claude plugin validate context-bar # check a mod the way Claude Code will load it
claude plugin test context-bar # run its tests
IDEAS.md has ideas for more mods.
hooks/register.tsx 71 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Receipt } from '../types'
5import { fullLine } from './receipt'
6
7const byDuration = atom({ plugin: 'turn-receipt', key: 'byDuration' } as const, {})
8
9async function spent($: EngineInterface) {
10 return (await $.session.usage()).cost?.usd ?? null
11}
12
13export const register: Register = on => {
14 // The turn in progress; a reload mid-turn just under-counts that one turn.
15 let tools = 0
16 let files = new Set<string>()
17 let usdAtStart: number | null = null
18
19 on('prompt.submit', async ($, e, next) => {
20 tools = 0
21 files = new Set()
22 usdAtStart = await spent($)
23 return next(e)
24 })
25
26 on('tool.call', async ($, e, next) => {
27 tools += 1
28 const ran = await next(e)
29 if (!ran.isError && (e.tool === 'Edit' || e.tool === 'Write')) files.add(e.file_path)
30 if (!ran.isError && e.tool === 'NotebookEdit') files.add(e.notebook_path)
31 return ran
32 })
33
34 on('turn.complete', async ($, e, next) => {
35 if (e.agentId) return next(e)
36 const usd = await spent($)
37 const receipt: Receipt = {
38 ms: e.durationMs,
39 tools,
40 files: [...files],
41 usd: usd !== null && usdAtStart !== null ? usd - usdAtStart : null,
42 isAborted: e.isAborted,
43 doneAt: Date.now(),
44 tokensIn: e.usage ? e.usage.input_tokens + e.usage.cache_read_input_tokens + e.usage.cache_creation_input_tokens : null,
45 tokensOut: e.usage?.output_tokens ?? null,
46 }
47 // ponytail: two turns with the same durationMs share a receipt; key by a real turn id if the line ever carries one.
48 await update($, byDuration, all => Object.fromEntries([...Object.entries(all), [String(e.durationMs), receipt]].slice(-500)))
49 return next(e)
50 })
51
52 // Redraws the engine's end-of-turn line ("✻ Baked for 42s · done 10:21 PM") with the receipt on the end.
53 // The engine's own line is an opaque handle a mod can't add to, so this draws the whole line.
54 on('ui.render', { component: 'TurnDuration' }, async ($, e, next) => {
55 const receipt = (await read($, byDuration))[String(e.props.durationMs)]
56 if (!receipt?.doneAt) return next(e) // receipts saved before doneAt existed keep the plain line
57 const { Box, Text } = $.ui.resolve(e)
58 // The tldr mod's line for this turn goes under the receipt: this hook replaces the line tldr would add it to.
59 const tldr = ((await $.state.get({ plugin: 'tldr', key: 'byDuration' } as never)) as { value?: Record<string, string> }).value
60 const summary = tldr?.[String(e.props.durationMs)]
61 const line = <Text dimColor>{fullLine(e.props.word, receipt)}</Text>
62 if (!summary) return line
63 return (
64 <Box flexDirection="column">
65 {line}
66 <Text dimColor> TL;DR: {summary}</Text>
67 </Box>
68 )
69 })
70}
71hooks/receipt.ts 27 lines1import type { Receipt } from '../types'
2
3// The tail after the engine's "✻ Baked for 42s · done 10:21 PM".
4export function tail(r: Receipt): string {
5 const parts = [`${r.tools} tool${r.tools === 1 ? '' : 's'}`]
6 if (r.files.length) parts.push(`${r.files.length} file${r.files.length === 1 ? '' : 's'} edited: ${r.files.map(f => f.split('/').pop()).join(', ')}`)
7 if (r.tokensIn != null && r.tokensOut != null) // != also skips receipts saved before tokens existed
8 parts.push(`${kTokens(r.tokensIn)} in / ${kTokens(r.tokensOut)} out`)
9 if (r.usd !== null) parts.push(`$${r.usd.toFixed(2)}`)
10 return ` · ${parts.join(' · ')}`
11}
12
13export function kTokens(n: number): string {
14 return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`
15}
16
17export function duration(ms: number): string {
18 const s = Math.round(ms / 1000)
19 return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`
20}
21
22// The engine's "✻ Baked for 42s · done 10:21 PM", then the receipt.
23export function fullLine(word: string, r: Receipt): string {
24 const done = new Date(r.doneAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
25 return `✻ ${word} for ${duration(r.ms)} · done ${done}${tail(r)}`
26}
27types/index.d.ts 9 lines1export type Receipt = { ms: number; tools: number; files: string[]; usd: number | null; isAborted: boolean; doneAt: number; tokensIn: number | null; tokensOut: number | null }
2
3declare module 'claude-code' {
4 interface PluginState {
5 // Keyed by the turn's durationMs: the one thing the end-of-turn line shares with turn.complete.
6 'turn-receipt': { byDuration: Record<string, Receipt> }
7 }
8}
9