SLOPSHOPPER

bang-run

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

newrowsstatusprocess
v0.1.0no licenseupdated 2026-10-13mitja-kleider/claude-mods/mods/bang-run
A shopper browsing a rack in a slop shop
README

bang-run

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-mods
Source 1 files
hooks/register.tsx 69 lines
1import 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