Queue prompts with /queue while Claude works, then run them one by one after each task and its background jobs finish.

Queue up your next prompts while Claude is still working. Run them later, by hand or automatically one by one.
Claude Code's own queue sends your message into the running task. This mod holds prompts back until you want them.
At the Claude Code prompt:
/plugin install prompt-queue --marketplace kuzmany/claude-code-prompt-queue
Type y to add the marketplace, then pick a scope.
From a shell:
claude plugin install prompt-queue --marketplace kuzmany/claude-code-prompt-queue
In the desktop app: click + next to the prompt box, then Plugins, Add marketplace with kuzmany/claude-code-prompt-queue, then Add plugin.
| Command | What it does |
|---|---|
/queue <text> | Park a prompt. Works while Claude is busy. |
/queue list | Show parked prompts. |
/queue run [n] | Send prompt n now (default 1). |
/queue drop <n> | Remove prompt n. |
/queue clear | Remove all prompts. |
/queue-auto | Toggle auto mode. |
/queue unblock | Stop waiting on background jobs. |
Auto mode is off by default. When on, the next prompt starts only after the current task ends and every background job or agent it started has finished. Then the next, until the queue is empty. Auto stays on until you toggle it off. Pressing Esc turns it off.
The status line shows the state, for example 3 queued · auto · waiting on 1 bg.
The queue belongs to the current session.
If auto keeps waiting on a job that already ended (for example one a subagent started), run /queue unblock.
> run the full test suite in the background
/queue fix the failing tests
/queue update the changelog
/queue-auto
Claude finishes the test run, then fixes the tests, then updates the changelog.
MIT
hooks/register.ts 184 lines1import { atom, read, update } from 'claude-code'
2import type { Engine, Register } from 'claude-code'
3
4const items = atom({ plugin: 'prompt-queue', key: 'items' } as const, [])
5const auto = atom({ plugin: 'prompt-queue', key: 'auto' } as const, false)
6// Ids of background tasks and agents the main session started and still waits on.
7const waiting = atom({ plugin: 'prompt-queue', key: 'waiting' } as const, [])
8// Whether a main-loop turn is running; kept in state so a reload mid-turn remembers it.
9const busy = atom({ plugin: 'prompt-queue', key: 'busy' } as const, false)
10
11const BASH_STARTED = /running in background with ID: (\S+?)\.?\s/
12const AGENT_STARTED = /agentId: ([\w-]+)/
13// A finished job's notice; it stays inside its own <task-notification> block.
14const FINISHED = /<task-id>([^<]+)<\/task-id>(?:(?!<\/task-notification>)[\s\S])*?<status>(?!running)/g
15// A subagent's final report, sent as a message from that agent.
16const HANDED_BACK = /<agent-message from="([^"]+)">\s*\[Subagent hand-back\]/g
17// Job notices arrive on their own or inside another session's message, never in a prompt the user typed.
18const NOTICE_ORIGINS = ['task-notification', 'peer']
19
20const HELP = `/queue <text> park a prompt
21/queue list show parked prompts
22/queue run [n] send prompt n now (default 1)
23/queue drop <n> remove prompt n
24/queue clear remove all
25/queue unblock stop waiting on background jobs
26/queue-auto toggle: run the queue after each task and its background jobs end`
27
28const showStatus = async ($: Engine) => {
29 const count = (await read($, items)).length
30 const jobs = (await read($, waiting)).length
31 const isAuto = await read($, auto)
32 const text = [
33 count && `${count} queued`,
34 isAuto && 'auto',
35 isAuto && count && jobs && `waiting on ${jobs} bg`,
36 ]
37 .filter(Boolean)
38 .join(' · ')
39 $.ui.status(text || undefined)
40}
41
42const setItems = async ($: Engine, fn: (list: string[]) => string[]) => {
43 await update($, items, fn)
44 await showStatus($)
45}
46
47const setWaiting = async ($: Engine, fn: (ids: string[]) => string[]) => {
48 await update($, waiting, fn)
49 await showStatus($)
50}
51
52// A hook cannot submit while it holds the turn; send once it has ended.
53const send = async ($: Engine, text: string) => {
54 await update($, busy, () => true)
55 $.clock.after(0, async () => {
56 try {
57 await $.prompt.submit({ text, asUser: true })
58 } catch {
59 // A failed send started no turn, so nothing would clear the flag.
60 await update($, busy, () => false)
61 }
62 })
63}
64
65// Auto sends the next prompt only when no turn runs and no background job is left.
66const sendIfIdle = async ($: Engine) => {
67 if ((await read($, busy)) || !(await read($, auto)) || (await read($, waiting)).length) return
68 const [first] = await read($, items)
69 if (first === undefined) return
70 await setItems($, list => list.slice(1))
71 await send($, first)
72}
73
74export const register: Register = on => {
75 on('session.start', async ($, e, next) => {
76 await $.command.register({
77 name: 'queue',
78 description: 'Park a prompt to run later (list, run, drop, clear)',
79 immediate: true,
80 })
81 await $.command.register({
82 name: 'queue-auto',
83 description: 'Toggle running the queue after each task and its background jobs end',
84 immediate: true,
85 })
86 await showStatus($)
87
88 return next(e)
89 })
90
91 on('turn.start', async ($, e, next) => {
92 if (e.agentId === undefined) await update($, busy, () => true)
93
94 return next(e)
95 })
96
97 on('tool.call', async ($, e, next) => {
98 const ran = await next(e)
99 // Only a background Bash call or an Agent call starts a job; other output may quote the words.
100 const pattern =
101 e.tool === 'Bash' && e.run_in_background ? BASH_STARTED : e.tool === 'Agent' ? AGENT_STARTED : undefined
102 const id = pattern && ran.text?.match(pattern)?.[1]
103 if (id) await setWaiting($, ids => [...ids.filter(one => one !== id), id])
104
105 return ran
106 })
107
108 on('prompt.submit', async ($, e, next) => {
109 if (NOTICE_ORIGINS.includes(e.origin.kind)) {
110 const ended = [...e.text.matchAll(FINISHED), ...e.text.matchAll(HANDED_BACK)].map(match => match[1])
111 if (ended.length) await setWaiting($, ids => ids.filter(one => !ended.includes(one)))
112 }
113
114 return next(e)
115 })
116
117 on('turn.complete', async ($, e, next) => {
118 const ended = await next(e)
119 if (e.agentId !== undefined) return ended
120 await update($, busy, () => false)
121
122 // Esc turns auto off instead of starting the next prompt.
123 if (e.isAborted && (await read($, auto))) {
124 await update($, auto, () => false)
125 await showStatus($)
126 $.ui.toast('Queue auto is off. /queue-auto turns it back on.')
127 } else {
128 await sendIfIdle($)
129 }
130
131 return ended
132 })
133
134 on('command.run', { command: 'queue-auto' }, async $ => {
135 const isOn = !(await read($, auto))
136 await update($, auto, () => isOn)
137 await showStatus($)
138 if (!isOn) return { text: 'Queue auto is off.' }
139 await sendIfIdle($)
140
141 return { text: 'Queue auto is on: prompts run after each task and its background jobs end.' }
142 })
143
144 on('command.run', { command: 'queue' }, async ($, e) => {
145 const args = e.args.trim()
146 // A subcommand is matched only in its exact form, so "/queue list the files" is queued.
147 const found = args.match(/^(help|list|clear|unblock)$|^(run|drop)(?:\s+(\d+))?$/)
148 const word = found?.[1] ?? found?.[2]
149 const arg = found?.[3] ?? ''
150 const list = await read($, items)
151 const n = Number(arg || '1')
152 const i = Number.isInteger(n) && n >= 1 && n <= list.length ? n - 1 : -1
153
154 if (args === '' || word === 'help') return { text: HELP }
155
156 if (word === 'list') {
157 return { text: list.map((t, j) => `${j + 1}. ${t}`).join('\n') || 'Queue is empty.' }
158 }
159
160 if (word === 'clear') {
161 await setItems($, () => [])
162 return { text: `Cleared ${list.length} prompt(s).` }
163 }
164
165 if (word === 'unblock') {
166 await setWaiting($, () => [])
167 await sendIfIdle($)
168 return { text: 'Stopped waiting on background jobs.' }
169 }
170
171 if (word === 'drop' || word === 'run') {
172 if (i < 0) return { text: `No prompt ${arg || '1'}. Queue has ${list.length}.` }
173 await setItems($, l => l.filter((_, j) => j !== i))
174 if (word === 'run') await send($, list[i])
175 return { text: `${word === 'run' ? 'Sending' : 'Dropped'}: ${list[i]}` }
176 }
177
178 await setItems($, l => [...l, args])
179 await sendIfIdle($)
180
181 return { text: `Queued #${list.length + 1}.` }
182 })
183}
184types/index.d.ts 8 lines1export type QueueItems = string[]
2
3declare module 'claude-code' {
4 interface PluginState {
5 'prompt-queue': { items: QueueItems; auto: boolean; waiting: string[]; busy: boolean }
6 }
7}
8