SLOPSHOPPER

blocked-run

When auto mode blocks a command, shows it above the prompt with a button to run it yourself

newbandtoastpromptprocess
★ 1v0.1.0MITupdated 2026-10-06Spardutti/claude-mods/plugins/blocked-run
A shopper browsing a rack in a slop shop
README

claude-mods

Little mods for Claude Code: panels that keep the stuff you'd normally go check next to the chat, and helpers that do the chores for you.

Claude Code 2.1.287+ mods: 4 License: MIT

Why

You spend all day in Claude Code. But the small stuff keeps pulling you out of it: checking git, rescuing a session that got too long, doing the same cleanup by hand.

These mods put that stuff inside Claude Code. Some draw a live panel next to the chat. Some do a chore for you at the right moment. You keep your eyes on the work.

Each one installs on its own, so you take only what you want.

Get started

Add the marketplace once, then install a mod:

/plugin marketplace add spardutti/claude-mods
/plugin install branch-status@spardutti-mods
/plugin install auto-handoff@spardutti-mods
/plugin install handoff-pickup@spardutti-mods
/plugin install blocked-run@spardutti-mods

Install any you like. Each one starts working on the next session start.

The mods

Each mod has its own page with the full story.

ModWhat it does
branch-statusA panel with your branch, develop and main as a tiny git-flow graph, so you see what's waiting to ship without running git log.
auto-handoffWhen the context gets too full, it warns you, counts down, then hands the work to a fresh session.
handoff-pickupReminds you of a handoff you left behind. /pickup picks it up, /pickup done deletes it.
blocked-runWhen auto mode blocks a command, it shows above the prompt with a button to run it yourself.
/plugin marketplace update spardutti-mods
/plugin update branch-status@spardutti-mods
/plugin update auto-handoff@spardutti-mods
/plugin update handoff-pickup@spardutti-mods
/plugin update blocked-run@spardutti-mods
/reload-plugins
Source 2 files
hooks/register.tsx 100 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderInput } from 'claude-code'
3
4import type { Blocked } from '../types'
5
6const MAX_OUTPUT = 4000
7const blocked = atom({ plugin: 'blocked-run', key: 'blocked' } as const, null as Blocked | null)
8
9let cwd: string | undefined
10
11const commandOf = (input: unknown) =>
12  typeof input === 'object' && input !== null && 'command' in input && typeof input.command === 'string' ? input.command : undefined
13
14const lastLineOf = (output: string) => output.trimEnd().split('\n').pop() ?? ''
15
16function noteFor(command: string, exitCode: number, output: string) {
17  const tail = output.length > MAX_OUTPUT ? `...${output.slice(-MAX_OUTPUT)}` : output
18  return `Auto mode blocked this command, so the user ran it themselves:\n$ ${command}\nExit code ${exitCode}.\nOutput:\n${tail || '(none)'}`
19}
20
21async function streamRun($: EngineInterface, command: string) {
22  const child = $.process.spawn({ argv: ['bash', '-c', command], cwd })
23  let output = ''
24  let step = await child.next()
25  while (!step.done) {
26    output += step.value.text
27    await update($, blocked, b => b && { ...b, lastLine: lastLineOf(output) })
28    step = await child.next()
29  }
30  return { exitCode: step.value.code ?? -1, output }
31}
32
33// Not awaited by the button: a press gets 10s, and a deploy runs for minutes.
34async function runIt($: EngineInterface, command: string) {
35  await update($, blocked, () => ({ command, status: 'running' }))
36  const { exitCode, output } = await streamRun($, command).catch(err => ({ exitCode: -1, output: String(err) }))
37  const note = noteFor(command, exitCode, output)
38  await update($, blocked, () => ({ command, status: 'done' as const, exitCode, lastLine: lastLineOf(output), note }))
39}
40
41function band($: EngineInterface, e: RenderInput<'AbovePrompt'>, shown: Blocked) {
42  const { Box, Button, Text } = $.ui.resolve(e)
43  const label = (text: string, color: string) => <Text backgroundColor={color} color="black" bold>{` ${text} `}</Text>
44  const dismiss = <Button key="dismiss" label="Dismiss" onPress={() => update($, blocked, () => null)} />
45  if (shown.status === 'waiting') {
46    return (
47      <Box flexDirection="column">
48        <Text>{label('AUTO MODE BLOCKED', 'yellow')} <Text bold>{shown.command}</Text></Text>
49        <Box>
50          <Button key="run" label="Run it myself" onPress={() => void runIt($, shown.command)} />
51          <Text> </Text>
52          {dismiss}
53        </Box>
54      </Box>
55    )
56  }
57  if (shown.status === 'running') {
58    return <Text>{label('RUNNING', 'cyan')} <Text dimColor>{shown.lastLine || shown.command}</Text></Text>
59  }
60  const isOk = shown.exitCode === 0
61  return (
62    <Box flexDirection="column">
63      <Text>{label(isOk ? 'DONE' : `FAILED (exit ${shown.exitCode})`, isOk ? 'green' : 'red')} <Text dimColor>{shown.lastLine}</Text></Text>
64      <Box>
65        <Text>Claude sees the output with your next message. </Text>
66        {dismiss}
67      </Box>
68    </Box>
69  )
70}
71
72export const register: Register = on => {
73  on('session.start', async ($, e, next) => {
74    cwd = e.cwd
75    return next(e)
76  })
77
78  on('classic.PermissionDenied', async ($, e, next) => {
79    const command = e.tool_name === 'Bash' ? commandOf(e.tool_input) : undefined
80    if (command) {
81      await update($, blocked, () => ({ command, status: 'waiting' }))
82      $.ui.toast('Auto mode blocked a command. Run it yourself from above the prompt.')
83    }
84    return next(e)
85  })
86
87  on('prompt.submit', async ($, e, next) => {
88    const note = (await read($, blocked))?.note
89    if (!note) return next(e)
90    const context = [...(e.context ?? []), note]
91    await update($, blocked, () => null)
92    return next({ ...e, context })
93  })
94
95  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
96    const shown = await read($, blocked)
97    return e.props.hasSurvey || !shown ? next(e) : band($, e, shown)
98  })
99}
100
types/index.d.ts 8 lines
1export type Blocked = { command: string; status: 'waiting' | 'running' | 'done'; lastLine?: string; exitCode?: number; note?: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'blocked-run': { blocked: Blocked | null }
6  }
7}
8