Mod + skill: /lgtm is your auto-approval gate for one turn (variants: safe, decide, plan, push, yolo). Hooks into your-call and are-you-sure-bro; a chip shows…

Your auto-approval gate. /lgtm pre-approves the questions Claude would otherwise ask you, for the next turn only.
| Variant | Approves |
|---|---|
/lgtm (same as /lgtm safe) | picks the recommended option on your-call questions; low/medium-risk plan items (no commit/push, no deletes) |
/lgtm decide | recommended options only |
/lgtm plan | plan items only |
/lgtm push | safe, plus commit/push plan items |
/lgtm yolo (or /lgtm fuck it) | everything, dangerous commands and sensitive writes included |
/lgtm off closes it, /lgtm log lists what it approved, /lgtm status says where it stands. A bare /lgtm arms the defaultVariant option (safe).
How it behaves:
/lgtm arms it; it opens when your next prompt starts the turn and closes when the turn ends. Nothing is approved before that.✓ lgtm safe · 3 auto-approved, ⚠ for yolo) with an Off button; a toast at the end of the turn; /lgtm log lists every approval.your-call (picks, and plan items from its approve tool) and are-you-sure-bro (dangerous commands, sensitive writes) through shared state: the gate publishes what it allows, they read it and record what they approved. Without this plugin they simply ask.rm -rf / or ~, mkfs, dd onto a device, DROP DATABASE) always ask.The lgtm skill covers the other meaning: you typing "lgtm" about something just shown approves exactly that.
hooks/register.tsx 125 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Approval } from '../types'
5
6import { RANK, chipKey, injectChips, isEmpty } from './chips'
7import { DESCRIPTIONS, VARIANTS, matrixFor, parse } from './policy'
8import type { Variant } from './policy'
9
10const gate = atom({ plugin: 'looks-good-to-me', key: 'gate' } as const, { mode: null, phase: 'off', since: 0, allow: null })
11
12// The plugins that act on the gate record what they approved in their own state; this one reads it.
13const askApprovals = { plugin: 'are-you-sure-bro', key: 'approvals' } as const
14const callApprovals = { plugin: 'your-call', key: 'approvals' } as const
15
16export const countSince = (lists: Approval[][], since: number): number => lists.reduce((n, l) => n + l.filter(a => a.at >= since).length, 0)
17
18export const logText = (items: Approval[]): string =>
19 items.length === 0
20 ? 'No auto-approvals this turn or last.'
21 : [...items]
22 .sort((a, b) => a.at - b.at)
23 .map((a, i) => `${i + 1}. ${a.kind}: ${a.detail.slice(0, 80)}`)
24 .join('\n')
25
26const readAll = async ($: any): Promise<Approval[]> => {
27 const a = (await $.state.get(askApprovals)).value ?? []
28 const c = (await $.state.get(callApprovals)).value ?? []
29 return [...a, ...c]
30}
31
32export const register: Register = (on, options) => {
33 const dflt: Variant = (VARIANTS as string[]).includes(String(options.defaultVariant)) ? (options.defaultVariant as Variant) : 'safe'
34 let last: Approval[] = []
35
36 on('session.start', async ($, e, next) => {
37 await $.command.register({
38 name: 'lgtm',
39 description: 'Auto-approval gate for the next turn: /lgtm [safe|decide|plan|push|yolo|off|log]',
40 })
41 return next(e)
42 })
43
44 on('command.run', { command: 'lgtm' }, async ($, e) => {
45 const p = parse(e.args)
46
47 if (p.cmd === 'log') {
48 const g = await read($, gate)
49 const mine = (await readAll($)).filter(a => a.at >= g.since)
50 return { text: logText(mine.length ? mine : last) }
51 }
52 if (p.cmd === 'off') {
53 await update($, gate, g => ({ mode: null, phase: 'off', since: g.since, allow: null }))
54 return { text: 'lgtm off.' }
55 }
56 if (p.cmd === 'status') {
57 const g = await read($, gate)
58 return { text: g.mode ? `lgtm ${g.mode} (${g.phase}): ${DESCRIPTIONS[g.mode]}` : `lgtm is off. Variants: ${VARIANTS.join(', ')}.` }
59 }
60 if (p.cmd === 'unknown') return { text: `Unknown "${p.word}". Variants: ${VARIANTS.join(', ')}, or off, log, status.` }
61 if (p.cmd !== 'arm') return { text: 'Usage: /lgtm [safe|decide|plan|push|yolo|off|log]' }
62
63 const mode = p.mode ?? dflt
64 // Armed: it opens when the next turn starts. Nothing is approved before that.
65 await update($, gate, g => ({ mode, phase: 'armed', since: g.since, allow: null }))
66 return { text: `lgtm ${mode} for the next turn: ${DESCRIPTIONS[mode]}.` }
67 })
68
69 // The gate opens when the turn starts and closes when it ends: "this turn only".
70 on('prompt.submit', async ($, e, next) => {
71 const g = await read($, gate)
72 if (g.phase === 'armed' && g.mode && e.origin === undefined) {
73 const now = await $.clock.now()
74 await update($, gate, () => ({ mode: g.mode, phase: 'active', since: now, allow: matrixFor(g.mode) }))
75 }
76 return next(e)
77 })
78
79 on('turn.complete', async ($, e, next) => {
80 const g = await read($, gate)
81 if (g.phase === 'active') {
82 const mine = (await readAll($)).filter(a => a.at >= g.since)
83 last = mine
84 if (mine.length > 0) $.ui.toast(`lgtm ${g.mode}: ${mine.length} auto-approved.\n/lgtm log lists them`)
85 await update($, gate, () => ({ mode: null, phase: 'off', since: g.since, allow: null }))
86 }
87 return next(e)
88 })
89
90 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
91 const g = await read($, gate)
92
93 if (!g.mode || e.props.hasSurvey) return next(e)
94
95 const a = (await $.state.get(askApprovals)).value ?? []
96 const c = (await $.state.get(callApprovals)).value ?? []
97 const count = g.phase === 'active' ? countSince([a, c], g.since) : 0
98 const { Box, Text, Button } = $.ui.resolve(e)
99 const rest = await next(e)
100 const hot = g.mode === 'yolo'
101 const auto = count > 0 ? ` · ${count} auto-approved` : ''
102 const note = g.phase === 'armed' ? ' (next turn)' : auto
103 const chip = (
104 <Box key={chipKey(RANK.gate, 'lgtm')}>
105 <Text key="lgtm" color={hot ? 'red' : 'green'} bold>
106 {hot ? '⚠' : '✓'} lgtm {g.mode}
107 {note}{' '}
108 </Text>
109 <Button key="lgtm-off" label="Off" onPress={() => update($, gate, x => ({ mode: null, phase: 'off', since: x.since, allow: null }))} />
110 </Box>
111 )
112 const joined = injectChips(rest, chip)
113
114 if (joined) return joined as never
115 if (isEmpty(rest)) return <Box key="chips" flexWrap="wrap">{chip}</Box>
116
117 return (
118 <Box flexDirection="column">
119 <Box key="chips" flexWrap="wrap">{chip}</Box>
120 {rest}
121 </Box>
122 )
123 })
124}
125hooks/chips.ts 42 lines1// Chip row: small status chips from different plugins share one row above the prompt.
2// - The row is a Box with props.key === 'chips' (wrapping, so a narrow terminal gets a second line
3// instead of cut-off buttons).
4// - Each chip is a Box keyed `chip:<rank>:<name>`; chips sort by rank so the order is the same
5// whichever plugin happens to draw first: gate 5, context 10, route 20, result 30.
6// Plain data in, plain data out, so it works on whatever `next(e)` returns.
7export type Node = { type?: string; props?: Record<string, unknown>; children?: unknown[] }
8
9export const CHIPS_KEY = 'chips'
10export const GAP = ' '
11export const RANK = { gate: 5, context: 10, route: 20, result: 30 } as const
12
13export const chipKey = (rank: number, name: string): string => `chip:${String(rank).padStart(3, '0')}:${name}`
14
15const isNode = (n: unknown): n is Node => typeof n === 'object' && n !== null
16
17const keyOf = (n: unknown): string => (isNode(n) && typeof n.props?.key === 'string' ? n.props.key : '')
18
19/** Chips in rank order with a gap between each. */
20export const arrange = (chips: unknown[]): unknown[] =>
21 [...chips]
22 .sort((a, b) => keyOf(a).localeCompare(keyOf(b)))
23 .flatMap((c, i) => (i === 0 ? [c] : [GAP, c]))
24
25/** Add `chip` to the chips row inside `node`; undefined when there is none. */
26export const injectChips = (node: unknown, chip: unknown): Node | undefined => {
27 if (!isNode(node)) return undefined
28 if (node.props?.key === CHIPS_KEY) {
29 const existing = (node.children ?? []).filter(c => c !== GAP)
30 return { ...node, children: arrange([...existing, chip]) }
31 }
32 const kids = node.children ?? []
33 for (let i = kids.length - 1; i >= 0; i--) {
34 const hit = injectChips(kids[i], chip)
35 if (hit) return { ...node, children: kids.map((c, j) => (j === i ? hit : c)) }
36 }
37 return undefined
38}
39
40/** True when the band beneath drew nothing. */
41export const isEmpty = (node: unknown): boolean => node == null || (isNode(node) && (node.children ?? []).length === 0)
42hooks/policy.ts 85 lines1import type { Matrix } from '../types'
2
3// What each /lgtm variant is allowed to approve on your behalf. Pure, so it can be tested to death.
4export type Variant = 'safe' | 'decide' | 'plan' | 'push' | 'yolo'
5export const VARIANTS: Variant[] = ['safe', 'decide', 'plan', 'push', 'yolo']
6
7/** Other ways to say the same variant. `fuck it` and `fuckit` are yolo; `ship` was the old name of push. */
8export const ALIASES: Record<string, Variant> = { ship: 'push', fuckit: 'yolo', 'fuck it': 'yolo' }
9
10export type Kind = 'pick' | 'plan' | 'danger' | 'sensitive'
11export type Risk = 'low' | 'medium' | 'high'
12export type Action = 'read' | 'edit' | 'ship' | 'delete' | 'other'
13export type Info = { risk?: Risk; action?: Action; recommended?: boolean }
14
15export const DESCRIPTIONS: Record<Variant, string> = {
16 safe: 'picks the recommended option; approves low/medium-risk plan items (no commit/push, no delete)',
17 decide: 'picks the recommended option on your-call questions, nothing else',
18 plan: 'approves low/medium-risk plan items, nothing else',
19 push: 'safe, plus commit/push plan items',
20 yolo: 'approves everything, dangerous commands included (only catastrophic ones still ask); also /lgtm fuck it',
21}
22
23export const allow = (mode: Variant | null | undefined, kind: Kind, info: Info = {}): boolean => {
24 if (!mode) return false
25 // With nothing recommended there is nothing to pick for you, whatever the variant.
26 if (kind === 'pick') return mode !== 'plan' && info.recommended === true
27 if (mode === 'yolo') return true
28 if (kind === 'danger' || kind === 'sensitive') return false
29 // plan item
30 if (mode === 'decide') return false
31 const risk = info.risk ?? 'medium'
32 const action = info.action ?? 'other'
33 if (risk === 'high' || action === 'delete') return false
34 if (action === 'ship') return mode === 'push'
35 return true
36}
37
38export const matrixFor = (mode: Variant | null | undefined): Matrix => ({
39 pick: allow(mode, 'pick', { recommended: true }),
40 plan: {
41 medium: allow(mode, 'plan', { risk: 'medium', action: 'edit' }),
42 high: allow(mode, 'plan', { risk: 'high', action: 'edit' }),
43 ship: allow(mode, 'plan', { risk: 'low', action: 'ship' }),
44 delete: allow(mode, 'plan', { risk: 'low', action: 'delete' }),
45 },
46 danger: allow(mode, 'danger'),
47 sensitive: allow(mode, 'sensitive'),
48})
49
50/**
51 * The same decision as `allow`, from the published flags. Consumers (your-call, are-you-sure-bro)
52 * keep a copy of this function; the tests here pin them to `allow` for every variant and input.
53 */
54export const permits = (m: Matrix | null | undefined, kind: Kind, info: Info = {}): boolean => {
55 if (!m) return false
56 if (kind === 'danger') return m.danger
57 if (kind === 'sensitive') return m.sensitive
58 if (kind === 'pick') return m.pick && info.recommended === true
59 const risk = info.risk ?? 'medium'
60 const action = info.action ?? 'other'
61 return m.plan.medium && (risk !== 'high' || m.plan.high) && (action !== 'ship' || m.plan.ship) && (action !== 'delete' || m.plan.delete)
62}
63
64export type Parsed =
65 | { cmd: 'arm'; mode: Variant | undefined }
66 | { cmd: 'off' }
67 | { cmd: 'status' }
68 | { cmd: 'log' }
69 | { cmd: 'unknown'; word: string }
70
71/** `/lgtm` (safe), `/lgtm push`, `/lgtm fuck it`, `/lgtm off`, `/lgtm log`, `/lgtm status`. */
72export const parse = (args: string): Parsed => {
73 const words = args.trim().toLowerCase().split(/\s+/).filter(Boolean)
74 const w = words[0] ?? ''
75 if (!w) return { cmd: 'arm', mode: undefined }
76 const phrase = words.join(' ')
77 if (ALIASES[phrase]) return { cmd: 'arm', mode: ALIASES[phrase] }
78 if ((VARIANTS as string[]).includes(w)) return { cmd: 'arm', mode: w as Variant }
79 if (ALIASES[w]) return { cmd: 'arm', mode: ALIASES[w] }
80 if (w === 'off' || w === 'stop') return { cmd: 'off' }
81 if (w === 'status') return { cmd: 'status' }
82 if (w === 'log') return { cmd: 'log' }
83 return { cmd: 'unknown', word: w }
84}
85types/index.d.ts 22 lines1export type Variant = 'safe' | 'decide' | 'plan' | 'push' | 'yolo'
2export type Phase = 'off' | 'armed' | 'active'
3/** What the current variant allows, as plain flags the other plugins read. */
4export type Matrix = {
5 pick: boolean
6 plan: { medium: boolean; high: boolean; ship: boolean; delete: boolean }
7 danger: boolean
8 sensitive: boolean
9}
10/** Published for the plugins that ask you things; they read it, only this plugin writes it. */
11export type GateState = { mode: Variant | null; phase: Phase; since: number; allow: Matrix | null }
12export type Approval = { at: number; kind: string; detail: string }
13
14declare module 'claude-code' {
15 interface PluginState {
16 'looks-good-to-me': { gate: GateState }
17 // Written by the plugins that act on the gate; read here for the count and the log.
18 'are-you-sure-bro': { approvals: Approval[] }
19 'your-call': { approvals: Approval[] }
20 }
21}
22