A live pre-commit checklist above the prompt: re-runs your typecheck, lint or any command after Claude edits files.

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 157 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Check } from '../types'
5
6const checksAtom = atom({ plugin: 'preflight', key: 'checks' } as const, [] as Check[])
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 EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
41const GLYPH = { idle: '·', running: '…', pass: '✓', fail: '✗' } as const
42const COLOR = { idle: '#6b7280', running: '#f59e0b', pass: '#22c55e', fail: '#ef4444' } as const
43
44type Def = { name: string; command: string }
45
46let defs: Def[] = []
47let timer: { cancel: () => void } | null = null
48let isRunning = false
49let isDirty = false
50
51async function shell($: any, command: string) {
52 try {
53 return await $.process.run(['sh', '-c', command], { timeoutMs: 300000 })
54 } catch {
55 return await $.process.run(['cmd', '/c', command], { timeoutMs: 300000 })
56 }
57}
58
59// No setting given: use tsc when there is a tsconfig, and any lint script.
60async function detect($: any): Promise<Def[]> {
61 const found: Def[] = []
62 try {
63 const probe = await shell($, 'test -f tsconfig.json || dir tsconfig.json')
64 if (probe.exitCode === 0) found.push({ name: 'types', command: 'npx tsc -b' })
65 } catch {
66 // no shell: skip
67 }
68 try {
69 const r = await shell($, 'npm pkg get scripts.lint')
70 if (r.exitCode === 0 && r.stdout.trim() && r.stdout.trim() !== '{}') found.push({ name: 'lint', command: 'npm run lint' })
71 } catch {
72 // no npm: skip
73 }
74 return found
75}
76
77async function runAll($: any) {
78 if (isRunning) {
79 isDirty = true
80 return
81 }
82 isRunning = true
83 do {
84 isDirty = false
85 await update($, checksAtom, () => defs.map(d => ({ name: d.name, state: 'running' as const, issues: 0, at: 0 })))
86 for (let i = 0; i < defs.length; i++) {
87 let r
88 try {
89 r = await shell($, defs[i].command)
90 } catch {
91 r = { exitCode: 1, stdout: '', stderr: 'error: could not run' }
92 }
93 const out = `${r.stdout}\n${r.stderr}`
94 const issues = r.exitCode === 0 ? 0 : Math.max(1, (out.match(/\berror\b/gi) ?? []).length)
95 await update($, checksAtom, prev =>
96 prev.map((c, k) => (k === i ? { ...c, state: r.exitCode === 0 ? ('pass' as const) : ('fail' as const), issues, at: Date.now() } : c)),
97 )
98 }
99 } while (isDirty)
100 isRunning = false
101}
102
103export const register: Register = (on, options) => {
104 const delayMs = Math.max(1, Number(options.delaySeconds ?? 8)) * 1000
105
106 const parse = (s: string): Def[] =>
107 s
108 .split(';')
109 .map(p => p.trim())
110 .filter(Boolean)
111 .map(p => {
112 const i = p.indexOf('=')
113 return i > 0 ? { name: p.slice(0, i).trim(), command: p.slice(i + 1).trim() } : { name: p.split(' ')[0], command: p }
114 })
115
116 on('session.start', async ($, e, next) => {
117 defs = String(options.checks ?? '').trim() ? parse(String(options.checks)) : await detect($)
118 await update($, checksAtom, () => defs.map(d => ({ name: d.name, state: 'idle' as const, issues: 0, at: 0 })))
119 const result = await next(e)
120 $.clock.after(1000, () => runAll($))
121 return result
122 })
123
124 on('tool.call', async ($, e, next) => {
125 const result = await next(e)
126 if (EDIT_TOOLS.has(String(e.tool)) && defs.length > 0) {
127 timer?.cancel()
128 timer = $.clock.after(delayMs, () => runAll($))
129 }
130 return result
131 })
132
133 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
134 const below = await next(e)
135 const checks = await read($, checksAtom)
136 if (e.props.hasSurvey || checks.length === 0) return below
137
138 const { Box, Text } = $.ui.resolve(e)
139 const isWide = e.props.bodyColumns >= WIDE
140 const clock = (at: number) => (at ? new Date(at).toTimeString().slice(0, 5) : '')
141 const zone = (
142 <Box gap={2}>
143 {checks.map(c => (
144 <Box key={c.name} gap={1}>
145 <Text color={COLOR[c.state]}>{GLYPH[c.state]}</Text>
146 <Text dimColor={c.state === 'pass' || c.state === 'idle'}>{c.name}</Text>
147 {c.state === 'fail' ? <Text color="#ef4444">{c.issues}</Text> : null}
148 {c.state === 'idle' ? <Text dimColor>not run</Text> : null}
149 {isWide && c.at ? <Text dimColor>{clock(c.at)}</Text> : null}
150 </Box>
151 ))}
152 </Box>
153 )
154 return addZone(below, zone, 3)
155 })
156}
157types/index.d.ts 8 lines1export type Check = { name: string; state: 'idle' | 'running' | 'pass' | 'fail'; issues: number; at: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 preflight: { checks: Check[] }
6 }
7}
8