A live git strip above the prompt: branch, ahead/behind, changed files and the open pull request's checks.

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 110 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Repo } from '../types'
5
6const repoAtom = atom({ plugin: 'repo-status', key: 'repo' } as const, null as Repo | null)
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
40export const register: Register = (on, options) => {
41 const wantPr = options.showPullRequest !== false
42 let prText = ''
43 let prLong = ''
44 let prAt = 0
45
46 on('session.start', async ($, e, next) => {
47 const poll = async () => {
48 try {
49 const st = await $.process.run(['git', 'status', '--porcelain=v1', '-b'], { timeoutMs: 8000 })
50 if (st.exitCode !== 0) return update($, repoAtom, () => null)
51 const lines = st.stdout.split('\n').filter(Boolean)
52 const head = lines[0] ?? ''
53 const branch = head.replace(/^## /, '').replace(/^No commits yet on /, '').split('...')[0].split(' ')[0]
54 const ahead = Number(/ahead (\d+)/.exec(head)?.[1] ?? 0)
55 const behind = Number(/behind (\d+)/.exec(head)?.[1] ?? 0)
56
57 const now = await $.clock.now()
58 if (wantPr && now - prAt > 60000) {
59 prAt = now
60 try {
61 const pr = await $.process.run(['gh', 'pr', 'view', '--json', 'number,state,statusCheckRollup'], { timeoutMs: 15000 })
62 if (pr.exitCode === 0) {
63 const j = JSON.parse(pr.stdout)
64 const checks: Array<{ conclusion?: string; status?: string }> = j.statusCheckRollup ?? []
65 const bad = checks.some(c => ['FAILURE', 'CANCELLED', 'TIMED_OUT'].includes(c.conclusion ?? ''))
66 const pending = checks.some(c => c.status !== 'COMPLETED')
67 const mark = j.state === 'MERGED' ? 'merged' : bad ? '✗' : pending ? '…' : '✓'
68 prText = `PR #${j.number} ${mark}`
69 prLong = j.state === 'MERGED' ? 'merged' : bad ? 'checks failing' : pending ? 'checks running' : 'checks passing'
70 } else {
71 prText = ''
72 prLong = ''
73 }
74 } catch {
75 prText = ''
76 prLong = ''
77 }
78 }
79 await update($, repoAtom, () => ({ branch, ahead, behind, changed: lines.length - 1, pr: prText, prLong }))
80 } catch {
81 // git missing or timed out: keep the last reading
82 }
83 }
84 await poll()
85 $.clock.every(5000, poll)
86 return next(e)
87 })
88
89 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
90 const below = await next(e)
91 const repo = await read($, repoAtom)
92 if (e.props.hasSurvey || !repo) return below
93
94 const { Box, Text } = $.ui.resolve(e)
95 const isWide = e.props.bodyColumns >= WIDE
96 const dirty = repo.changed > 0
97 const prColor = repo.pr.includes('✗') ? '#ef4444' : repo.pr.includes('…') ? '#f59e0b' : '#22c55e'
98
99 const zone = (
100 <Box gap={2}>
101 <Text bold color="#a855f7">{repo.branch}</Text>
102 {repo.ahead > 0 || repo.behind > 0 || isWide ? <Text dimColor>↑{repo.ahead} ↓{repo.behind}</Text> : null}
103 <Text color={dirty ? '#f59e0b' : '#22c55e'}>{dirty ? `● ${repo.changed} changed` : '✓ clean'}</Text>
104 {repo.pr ? <Text color={prColor}>{isWide && repo.prLong ? `${repo.pr.split(' ').slice(0, 2).join(' ')} · ${repo.prLong}` : repo.pr}</Text> : null}
105 </Box>
106 )
107 return addZone(below, zone, 1)
108 })
109}
110types/index.d.ts 15 lines1export type Repo = {
2 branch: string
3 ahead: number
4 behind: number
5 changed: number
6 pr: string
7 prLong: string
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 'repo-status': { repo: Repo | null }
13 }
14}
15