A green/red environment badge above the prompt that turns red when Claude's tool calls touch anything that looks like production.

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 { Touch } from '../types'
5
6const touchAtom = atom({ plugin: 'env-guard', key: 'touch' } as const, null as Touch)
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 holdMs = Math.max(1, Number(options.holdMinutes ?? 10)) * 60000
42 const patterns: RegExp[] = []
43 for (const p of String(options.patterns ?? '\\bprod\\b,\\bproduction\\b').split(',')) {
44 const s = p.trim()
45 if (!s) continue
46 try {
47 patterns.push(new RegExp(s, 'i'))
48 } catch {
49 // an invalid pattern is skipped
50 }
51 }
52
53 // What a call is judged on: the command, URL or arguments it acts with, never
54 // the text of a file it edits or reads, and not a commit message or heredoc body.
55 const subject = (e: Record<string, unknown>): string => {
56 const tool = String(e.tool)
57 if (tool === 'Bash') {
58 return String(e.command ?? '')
59 .replace(/<<-?\s*['"]?(\w+)['"]?[\s\S]*?\n\s*\1\b/g, '')
60 .replace(/(-m|--message|--body|-b|--title|-t)\s+("[^"]*"|'[^']*')/g, '')
61 }
62 if (tool === 'WebFetch') return String(e.url ?? '')
63 if (tool.startsWith('mcp__')) {
64 const { tool: _t, tool_use_id: _i, ...args } = e
65 return `${tool} ${JSON.stringify(args)}`
66 }
67 return ''
68 }
69
70 const hitOf = (e: Record<string, unknown>): string | null => {
71 const text = subject(e)
72 if (!text) return null
73 for (const re of patterns) if (re.test(text)) return re.source
74 return null
75 }
76
77 on('tool.call', async ($, e, next) => {
78 const tool = String(e.tool)
79 const hit = hitOf(e as Record<string, unknown>)
80 if (hit) {
81 const at = await $.clock.now()
82 await update($, touchAtom, () => ({ at, tool, hit }))
83 $.ui.toast(`Production touched by ${tool} (matched "${hit}")`)
84 // clear the red badge once the hold time has passed
85 $.clock.after(holdMs + 1000, () => update($, touchAtom, t => (t && t.at === at ? { ...t } : t)))
86 }
87 return next(e)
88 })
89
90 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
91 const below = await next(e)
92 if (e.props.hasSurvey) return below
93
94 const touch = await read($, touchAtom)
95 const now = await $.clock.now()
96 const isRed = touch !== null && now - touch.at < holdMs
97
98 const { Box, Text } = $.ui.resolve(e)
99 const zone = (
100 <Box gap={1}>
101 <Text color={isRed ? '#ef4444' : '#22c55e'} bold>
102 {isRed ? '▲ PROD' : '● LOCAL'}
103 </Text>
104 {isRed && touch ? <Text dimColor>{touch.tool} matched "{touch.hit}"</Text> : null}
105 </Box>
106 )
107 return addZone(below, zone, 9)
108 })
109}
110types/index.d.ts 8 lines1export type Touch = { at: number; tool: string; hit: string } | null
2
3declare module 'claude-code' {
4 interface PluginState {
5 'env-guard': { touch: Touch }
6 }
7}
8