Adds a run button under every `! command` Claude suggests

Adds a ▶ run button under every ! command in Claude's replies, inline (` ! cmd ) or as a ! ` line in a code block.
A press runs the command through $SHELL -lc (ten minutes at most) and submits the command and its output as your prompt, so Claude sees it and replies.
Mods cannot enter the prompt's ! bash mode, so the command gets no terminal: interactive commands (gcloud auth login, tsh login) need to be typed with ! yourself.
/plugin install bang-run --marketplace mitja-kleider/claude-modshooks/register.tsx 69 lines1import type { Register } from 'claude-code'
2
3const INLINE = /`!\s*([A-Za-z0-9_.~/$"'-][^`\n]*?)\s*`/g
4const FENCE = /```[^\n]*\n([\s\S]*?)```/g
5const FENCED_LINE = /^\s*!\s*([A-Za-z0-9_.~/$"'-].*?)\s*$/
6
7const OUTPUT_LIMIT = 30000
8
9function bangCommands(text: string): string[] {
10 const found = new Set<string>()
11 for (const [, command = ''] of text.matchAll(INLINE)) found.add(command)
12 for (const [, body = ''] of text.matchAll(FENCE)) {
13 for (const line of body.split('\n')) {
14 const [, command] = FENCED_LINE.exec(line) ?? []
15 if (command) found.add(command)
16 }
17 }
18 return [...found]
19}
20
21function clip(text: string, limit: number): string {
22 return text.length > limit ? `${text.slice(0, limit)}\n… (${text.length - limit} more characters)` : text
23}
24
25export const register: Register = on => {
26 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
27 const commands = bangCommands(e.props.text)
28 if (commands.length === 0) return next(e)
29
30 const engine = await next(e)
31 const { Box, Button, Text } = $.ui.resolve(e)
32
33 const run = async (command: string) => {
34 const shell = (await $.env.get('SHELL')) ?? '/bin/sh'
35 $.ui.status(`running: ${command}`)
36 let stdout = ''
37 let stderr = ''
38 let exitCode: number
39 try {
40 const result = await $.process.run([shell, '-lc', command], { timeoutMs: 600000 })
41 ;({ stdout, stderr, exitCode } = result)
42 } catch (error) {
43 stderr = String(error)
44 exitCode = -1
45 } finally {
46 $.ui.status(undefined)
47 }
48
49 const output = [stdout.trimEnd(), stderr.trimEnd()].filter(Boolean).join('\n')
50 await $.prompt.submit({
51 text: `I ran \`${command}\` (exit ${exitCode}):\n\n\`\`\`\n${clip(output || '(no output)', OUTPUT_LIMIT)}\n\`\`\``,
52 asUser: true,
53 })
54 }
55
56 return (
57 <Box flexDirection="column">
58 {engine}
59 {commands.map((command, i) => (
60 <Box flexDirection="row" gap={1} marginLeft={2}>
61 <Button key={`run-${i}`} variant="primary" label="▶ run" onPress={() => run(command)} />
62 <Text dimColor>{command}</Text>
63 </Box>
64 ))}
65 </Box>
66 )
67 })
68}
69