When a turn dies on a dropped connection or a server error, asks Claude to try again, up to 5 times in a row.

A Claude Code mod for those API Error: Connection lost mid-response moments. When a turn dies on an API error, it waits a bit and sends "Oops, something seems to have gone wrong, can you try that again?" for you.
Runs inside Claude Code as a native mod: no tmux, no watcher process.
At the Claude Code prompt:
/plugin install oops-retry --marketplace lperezmo/oops-retry
Answer y to add the marketplace, then pick a scope.
From a shell:
claude plugin install oops-retry --marketplace lperezmo/oops-retry
In the desktop app: click + next to the prompt box, then Plugins, Add marketplace with lperezmo/oops-retry, then Add plugin.
claude plugin test .
MIT
hooks/register.ts 98 lines1import type { Register, Timer } from 'claude-code'
2
3const RETRY_TEXT = 'Oops, something seems to have gone wrong, can you try that again?'
4const MAX_RETRIES = 5
5// Seconds to wait before retry 1, 2, 3, 4 and 5.
6const DELAYS = [2, 5, 10, 20, 30]
7
8// Error kinds that will not clear by asking again.
9const HOPELESS = new Set([
10 'authentication_failed',
11 'oauth_org_not_allowed',
12 'account_on_hold',
13 'verification_required',
14 'billing_error',
15 'rate_limit',
16 'invalid_request',
17 'model_not_found',
18 'max_output_tokens',
19 'cloud_credential_error',
20])
21
22export const register: Register = on => {
23 let retries = 0
24 let lastErrorKind: string | undefined
25 let pending: Timer | undefined
26
27 const cancelPending = () => {
28 pending?.cancel()
29 pending = undefined
30 }
31
32 // StopFailure names the kind of API error; turn.complete only says "error".
33 on('classic.StopFailure', ($, e, next) => {
34 lastErrorKind = e.error
35
36 return next(e)
37 }).catch(($, e, next) => next(e))
38
39 // Anything the person sends themselves starts the count over.
40 on('prompt.submit', ($, e, next) => {
41 if (e.origin.kind === 'composer' || e.origin.kind === 'bridge') {
42 cancelPending()
43 retries = 0
44 }
45
46 return next(e)
47 }).catch(($, e, next) => next(e))
48
49 on('turn.complete', ($, e, next) => {
50 if (e.agentId !== undefined) {
51 return next(e)
52 }
53
54 if (e.reason !== 'error') {
55 // A clean answer, or the person interrupted: stop retrying.
56 cancelPending()
57 retries = 0
58 lastErrorKind = undefined
59
60 return next(e)
61 }
62
63 if (pending !== undefined) {
64 return next(e)
65 }
66
67 if (retries >= MAX_RETRIES) {
68 $.ui.toast(`Still failing after ${MAX_RETRIES} retries, over to you.`)
69 retries = 0
70
71 return next(e)
72 }
73
74 retries += 1
75 const attempt = retries
76 const seconds = DELAYS[attempt - 1] ?? 30
77 $.ui.toast(`Connection hiccup, retrying in ${seconds}s (${attempt}/${MAX_RETRIES})`)
78
79 // The kind can land after turn.complete, so check it when the timer fires.
80 pending = $.clock.after(seconds * 1000, () => {
81 pending = undefined
82 const kind = lastErrorKind
83 lastErrorKind = undefined
84
85 if (kind !== undefined && HOPELESS.has(kind)) {
86 $.ui.toast(`Not retrying: the API said ${kind}.`)
87 retries = 0
88
89 return
90 }
91
92 void $.prompt.submit({ text: RETRY_TEXT })
93 })
94
95 return next(e)
96 })
97}
98