Status dots above the prompt for your Docker Compose services and any local ports you name.

A small collection of Claude Code mods.
/plugin install <mod> --marketplace Arkboi-GG/claude-mods
The bars stack above the prompt, so you can install any combination.
skins mod, run /skin band off once so its own usage band doesn't show twice.git; gh is optional).web=5173,api=3000).checks) and shows pass/fail with an error count.LOCAL badge that turns red PROD when a tool call matches your production patterns, and toasts when it happens.Name: , e.g. Olivia: rewrite welcome email; map names to outfits with the outfits option.hooks/register.tsx 115 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Dot } from '../types'
5
6const dotsAtom = atom({ plugin: 'stack-health', key: 'dots' } as const, [] as Dot[])
7
8const WIDE = 110
9
10// Shared "dev row" protocol (the same helper sits in every mod of this repo):
11// each mod adds a keyed zone to one row, so the mods fill a single line together
12// and still work alone. Zones sort by their key, `zone-<order>`.
13function addZone(below: any, zone: any, order: number): any {
14 const tagged = { ...zone, props: { ...zone.props, key: `zone-${order}`, flexShrink: 0 } }
15 const byKey = (a: any, b: any) => String(a?.props?.key).localeCompare(String(b?.props?.key))
16 const merge = (node: any): any => {
17 if (!node || typeof node !== 'object') return null
18 if (node.props?.key === 'devrow') return { ...node, children: [...(node.children ?? []), tagged].sort(byKey) }
19 const kids: any[] = node.children ?? []
20 for (let i = 0; i < kids.length; i++) {
21 const hit = merge(kids[i])
22 if (hit) return { ...node, children: kids.map((k, j) => (j === i ? hit : k)) }
23 }
24 return null
25 }
26 const merged = merge(below)
27 if (merged) return merged
28 const row = {
29 type: 'Box',
30 props: { key: 'devrow', paddingX: 1, justifyContent: 'space-between', flexWrap: 'wrap', columnGap: 3 },
31 children: [tagged],
32 }
33 return {
34 type: 'Box',
35 props: { flexDirection: 'column' },
36 children: below ? [below, row] : [row],
37 }
38}
39
40const COLORS = { up: '#22c55e', wait: '#f59e0b', down: '#ef4444' } as const
41
42export const register: Register = (on, options) => {
43 const wantCompose = options.compose !== false
44 const ports = String(options.ports ?? '')
45 .split(',')
46 .map(s => s.trim())
47 .filter(Boolean)
48 .map(s => {
49 const [a, b] = s.split('=')
50 return b ? { name: a.trim(), port: b.trim() } : { name: `:${a}`, port: a }
51 })
52
53 on('session.start', async ($, e, next) => {
54 const poll = async () => {
55 const dots: Dot[] = []
56
57 if (wantCompose) {
58 try {
59 const r = await $.process.run(['docker', 'compose', 'ps', '--all', '--format', 'json'], { timeoutMs: 10000 })
60 if (r.exitCode === 0) {
61 const text = r.stdout.trim()
62 const rows: Array<{ Service?: string; State?: string; Health?: string; Status?: string }> = text.startsWith('[')
63 ? JSON.parse(text)
64 : text.split('\n').filter(Boolean).map(l => JSON.parse(l))
65 for (const row of rows) {
66 const state =
67 row.State === 'running' ? (row.Health === 'starting' ? 'wait' : row.Health === 'unhealthy' ? 'down' : 'up')
68 : row.State === 'restarting' || row.State === 'created' ? 'wait'
69 : 'down'
70 dots.push({ name: row.Service ?? '?', state, detail: (row.Status ?? '').replace(/^Up /i, '').replace(/ \(.*\)/, '').toLowerCase() })
71 }
72 }
73 } catch {
74 // docker not installed or daemon down: no compose dots
75 }
76 }
77
78 for (const p of ports) {
79 try {
80 await $.http.fetch(`http://localhost:${p.port}/`)
81 dots.push({ name: p.name, state: 'up', detail: `:${p.port}` })
82 } catch {
83 dots.push({ name: p.name, state: 'down', detail: `:${p.port}` })
84 }
85 }
86
87 await update($, dotsAtom, () => dots)
88 }
89 await poll()
90 $.clock.every(10000, poll)
91 return next(e)
92 })
93
94 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
95 const below = await next(e)
96 const dots = await read($, dotsAtom)
97 if (e.props.hasSurvey || dots.length === 0) return below
98
99 const { Box, Text } = $.ui.resolve(e)
100 const isWide = e.props.bodyColumns >= WIDE
101 const zone = (
102 <Box gap={2}>
103 {dots.map(d => (
104 <Box key={d.name} gap={1}>
105 <Text color={COLORS[d.state]}>●</Text>
106 <Text dimColor={d.state === 'up'}>{d.name}</Text>
107 {isWide && d.detail ? <Text dimColor>{d.detail}</Text> : null}
108 </Box>
109 ))}
110 </Box>
111 )
112 return addZone(below, zone, 2)
113 })
114}
115types/index.d.ts 8 lines1export type Dot = { name: string; state: 'up' | 'down' | 'wait'; detail: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'stack-health': { dots: Dot[] }
6 }
7}
8