Fixing Dropped Prompts in Claude Code UI Mods Using Background Clocks
Calling prompt submission directly inside Claude Code mod buttons or command hooks silently fails to trigger turns. Decoupling the user click into a variable queue flushed by a session clock solves the silent drop. This pattern ensures extensions trigger agent turns consistently.

Why it matters
Prevents silently dropped prompts and killed background processes when building custom UI panels and slash commands for Claude Code.
TL;DR
- 01Never call $.prompt.submit directly inside onPress or command hooks due to silent context teardown.
- 02Buffer outbound requests in a module-level variable and flush them from a $.clock.every loop created in session.start.
- 03Store clock handles and call cancel() before re-creating them during plugin reloads triggered by $.config.set.
- 04Prevent button recreation mid-press by restricting $.ui.invalidate calls to state mutation events.
Key facts
- Impacted Versions
- Claude Code 2.1.286 to 2.1.288
- Recommended Clock Interval
- 1,000 ms
The Lifecycle Bug in Mod Event Handlers
When developing interactive panels for Claude Code desktop (tested on versions 2.1.286 to 2.1.288), calling $.prompt.submit({ text }) directly within an onPress callback or slash command hook triggers an intermittent failure. The API contract stipulates that $.prompt.submit awaits an idle session before initiating a turn, but when invoked inside the synchronous lifecycle of a UI press event, the execution context is frequently torn down before dispatch occurs. No error is thrown, and .catch blocks remain silent.
The Queue-and-Clock Workaround
To ensure reliable execution, decouple the UI interaction from prompt submission:
1. Store the payload in a top-level queue variable during onPress. 2. Instantiate a single clock via $.clock.every(1_000, ...) inside the session.start hook. 3. Consume and nullify the pending message from inside the timer tick function.
let toSend = null;
on('session.start', async ($, e, next) => {
$.clock.every(1_000, () => {
if (toSend) {
const text = toSend;
toSend = null;
$.prompt.submit({ text });
}
});
});Long-Running Processes and Re-Render Traps
The same lifecycle truncation affects external processes spawned via $.process.run (such as opening system utilities or playing notification chimes); dispatching them from the persistent clock prevents premature termination. Furthermore, avoid creating clocks inside configuration handlers: updating values with $.config.set can reload the plugin, causing timers to duplicate unless explicitly stopped with timer?.cancel?.(). Finally, guard against button recreation during active clicks by calling $.ui.invalidate('ui.render') only when state actually mutates.
Try it in 2 minutes
let toSend = null;
let timer = null;
on('session.start', async ($, e, next) => {
try { timer?.cancel?.(); } catch {}
timer = $.clock.every(1_000, () => {
if (toSend) {
const text = toSend;
toSend = null;
$.prompt.submit({ text });
}
});
});javascript
✓ When to use
- When building Claude Code desktop extension panels with interactive trigger buttons.
- When creating slash commands that initiate turns while running auxiliary background jobs.
✕ When NOT to use
- When writing standard terminal prompts or running commands outside custom UI extensions.
- When using interactive CLI prompts that do not extend the desktop mod interface.
What to do today
- Audit existing Claude Code plugins for direct $.prompt.submit calls in UI event handlers.
- Wrap background execution in a 1,000ms clock instantiated in session.start.
- Add explicit timer.cancel() calls before clock re-instantiation to prevent duplicate executions.