Park prompt drafts in a queue above the prompt box; send, edit or remove each when ready.

My mods, skills and plugins for coding agents. Skills use the open SKILL.md format, so they work in many harnesses. Mods are harness-only and are marked below.
| Plugin | What it does | Works in |
|---|---|---|
| prompt-queue | Park prompt drafts above the prompt box. Send, edit or remove each one later. | Claude Code only (mod) |
| worktree-pool | Pool of leased git worktree slots. Many agents work on one repo at once and never collide. Safe reclaim of idle slots. | Any harness (skills + Node CLI) |
| worktree-pool-hooks | Hooks for worktree-pool: block edits in the root checkout, enforce slot ownership, record heartbeats. | Claude Code and Codex (hooks) |
Running more than one coding agent on a repo breaks things: agents overwrite each other, every task reinstalls dependencies, and nobody knows which worktree is safe to delete. worktree-pool gives each agent its own leased slot, keeps installs warm, and frees idle slots only with proof that no work is lost. Abandoned work is saved to a branch first. Read why and how.
Claude Code (mods and plugins):
claude plugin marketplace add alizaeda/agent-toolkit
claude plugin install prompt-queue@agent-toolkit
claude plugin install worktree-pool@agent-toolkit
claude plugin install worktree-pool-hooks@agent-toolkit
Any harness (skills only; detects Claude Code, Codex, Cursor, Gemini CLI and more):
npx skills add alizaeda/agent-toolkit
Codex (plugins): add .agents/plugins/marketplace.json once a plugin with skills is listed. See templates/plugin/.
templates/plugin to plugins/<name>.plugins/<name>/skills/<skill>/SKILL.md (start from templates/skill)..claude-plugin/marketplace.json. The entry name must equal the plugin name..agents/plugins/marketplace.json (source.path starts with ./).claude plugin validate ., then update CHANGELOG.md and tag a version.MIT
hooks/register.tsx 234 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { QueueItem } from '../types'
5
6const items = atom({ plugin: 'prompt-queue', key: 'items' } as const, [])
7
8const STORE_KEY = 'items'
9const PREVIEW = 36
10
11const preview = (text: string): string => {
12 const flat = text.replace(/\s+/g, ' ').trim()
13
14 return flat.length > PREVIEW ? `${flat.slice(0, PREVIEW - 1)}…` : flat
15}
16
17type Dollar = EngineInterface
18
19async function persist($: Dollar, list: QueueItem[]) {
20 await $.store.set(STORE_KEY, list)
21}
22
23async function add($: Dollar, text: string): Promise<boolean> {
24 const clean = text.trim()
25
26 if (clean === '') {
27 return false
28 }
29
30 const id = `${await $.clock.now()}-${Math.random().toString(36).slice(2, 6)}`
31 const next = await update($, items, (list: QueueItem[]) => [...list, { id, text: clean }])
32 await persist($, next)
33
34 return true
35}
36
37async function remove($: Dollar, id: string) {
38 const next = await update($, items, (list: QueueItem[]) => list.filter(one => one.id !== id))
39 await persist($, next)
40}
41
42async function sendNow($: Dollar, item: QueueItem) {
43 await remove($, item.id)
44 void $.prompt.submit({ text: item.text, asUser: true })
45}
46
47async function setArmed($: Dollar, id: string, armed: boolean) {
48 const next = await update($, items, (list: QueueItem[]) =>
49 list.map(one => (one.id === id ? { ...one, armed } : one)),
50 )
51 await persist($, next)
52}
53
54async function sendNext($: Dollar): Promise<boolean> {
55 const list = (await read($, items)) as QueueItem[]
56 const item = list.find(one => one.armed)
57
58 if (!item) {
59 return false
60 }
61
62 await sendNow($, item)
63
64 return true
65}
66
67async function queueDraft($: Dollar) {
68 const { text } = await $.prompt.read()
69
70 if (!(await add($, text))) {
71 $.ui.toast('Prompt box is empty. Nothing to queue.')
72
73 return
74 }
75
76 await $.prompt.fill({ text: '', mode: 'replace' })
77 $.ui.toast('Queued.')
78}
79
80export const register: Register = on => {
81 let isBusy = false
82
83 on('prompt.submit', async ($, e, next) => {
84 isBusy = isBusy || !e.text.trimStart().startsWith('/')
85
86 return next(e)
87 })
88
89 on('turn.complete', async ($, e, next) => {
90 isBusy = await sendNext($)
91
92 return next(e)
93 })
94
95 on('session.start', async ($, e, next) => {
96 const saved = await $.store.get(STORE_KEY)
97
98 if (Array.isArray(saved)) {
99 await update($, items, () => saved as QueueItem[])
100 }
101
102 await $.command.register({
103 name: 'queue',
104 description: 'Queue a prompt: /queue <text> | send <n> | after <n> | edit <n> | rm <n> | clear',
105 })
106
107 return next(e)
108 })
109
110 on('command.run', { command: 'queue' }, async ($, e) => {
111 const args = e.args.trim()
112 const list = (await read($, items)) as QueueItem[]
113 const [verb, rest] = args.split(/\s+/, 2)
114 const pick = (): QueueItem | undefined => list[Number(rest) - 1]
115
116 if (args === '') {
117 if (list.length === 0) {
118 return { text: 'Queue is empty. Use /queue <text> or the Queue draft button.' }
119 }
120
121 return { text: list.map((one, i) => `${i + 1}. ${one.armed ? '[after] ' : ''}${preview(one.text)}`).join('\n') }
122 }
123
124 if (verb === 'clear') {
125 await update($, items, () => [])
126 await persist($, [])
127
128 return { text: 'Queue cleared.' }
129 }
130
131 if (verb === 'send' || verb === 'after' || verb === 'edit' || verb === 'rm') {
132 const item = pick()
133
134 if (!item) {
135 return { text: `No queue item "${rest ?? ''}". Use /queue to see the list.` }
136 }
137
138 if (verb === 'after') {
139 if (isBusy) {
140 await setArmed($, item.id, true)
141
142 return { text: `Item ${rest} will send when the agent is done.` }
143 }
144
145 isBusy = true
146 $.clock.after(50, () => void sendNow($, item))
147
148 return { text: `Agent is idle. Sent item ${rest}.` }
149 }
150
151 if (verb === 'send') {
152 isBusy = true
153 $.clock.after(50, () => void sendNow($, item))
154
155 return { text: `Sent item ${rest}.` }
156 }
157
158 await remove($, item.id)
159
160 if (verb === 'edit') {
161 await $.prompt.fill({ text: item.text, mode: 'replace' })
162
163 return { text: `Item ${rest} is in the prompt box.` }
164 }
165
166 return { text: `Removed item ${rest}.` }
167 }
168
169 await add($, args)
170
171 return { text: 'Queued.' }
172 })
173
174 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
175 if (e.props.hasSurvey) {
176 return next(e)
177 }
178
179 const list = (await read($, items)) as QueueItem[]
180 const { Box, Button, Text } = $.ui.resolve(e)
181
182 return (
183 <Box flexDirection="column" paddingX={1}>
184 <Box gap={1}>
185 <Button key="queue" label="+ Queue" onPress={() => queueDraft($)} />
186 {list.length > 0 && <Text dimColor>{list.length} waiting</Text>}
187 </Box>
188 {list.map(one => (
189 <Box key={one.id} gap={1} paddingLeft={2}>
190 <Text dimColor>
191 {one.armed ? '⏱ ' : ''}
192 {preview(one.text)}
193 </Text>
194 <Button
195 key={`send-${one.id}`}
196 label="Send"
197 onPress={async () => {
198 isBusy = true
199 await sendNow($, one)
200 }}
201 />
202 {one.armed ? (
203 <Button key={`hold-${one.id}`} label="Hold" onPress={() => setArmed($, one.id, false)} />
204 ) : (
205 <Button
206 key={`after-${one.id}`}
207 label="After"
208 onPress={async () => {
209 if (isBusy) {
210 await setArmed($, one.id, true)
211 $.ui.toast('Will send when the agent is done.')
212 } else {
213 isBusy = true
214 await sendNow($, one)
215 }
216 }}
217 />
218 )}
219 <Button
220 key={`edit-${one.id}`}
221 label="Edit"
222 onPress={async () => {
223 await remove($, one.id)
224 await $.prompt.fill({ text: one.text, mode: 'replace' })
225 }}
226 />
227 <Button key={`rm-${one.id}`} label="✕" onPress={() => remove($, one.id)} />
228 </Box>
229 ))}
230 </Box>
231 )
232 })
233}
234types/index.d.ts 8 lines1export type QueueItem = { id: string; text: string; armed?: boolean }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'prompt-queue': { items: QueueItem[] }
6 }
7}
8