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

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.
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.
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.
Each mod has its own page with the full story.
| Mod | What it does |
|---|---|
| branch-status | A 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-handoff | When the context gets too full, it warns you, counts down, then hands the work to a fresh session. |
| handoff-pickup | Reminds you of a handoff you left behind. /pickup picks it up, /pickup done deletes it. |
| blocked-run | When 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-pluginshooks/register.tsx 100 lines1import { 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}
100types/index.d.ts 8 lines1export 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