When the 5-hour usage limit is hit, wait for the reset and send "continue" automatically.

A Claude Code mod. Part of my wider collection, claude-mods.
Hit the 5-hour limit and walk away: this mod waits for the window to reset (plus a margin) and submits "continue" so the session carries on without you.
claude plugin marketplace add icedevil2001/auto-continue
claude plugin install auto-continue@auto-continue --scope user
Needs a Claude Code build with mods (tested on 2.1.286). Footprint: reads the engine's rate-limit usage, sets one timer, submits one prompt; no network, files or processes.
/auto-continue (toggle), /auto-continue on|off|status.| Option | Default | |
|---|---|---|
threshold | 25 | % of the 5-hour window at which the button appears |
graceMinutes | 5 | wait after the reset time before sending |
message | continue | the prompt sent on resume |
ponytail: note in hooks/register.tsx.hooks/register.tsx 126 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4const percent = atom({ plugin: 'auto-continue', key: 'percent' } as const, null)
5const isArmed = atom({ plugin: 'auto-continue', key: 'isArmed' } as const, false)
6const resumeAt = atom({ plugin: 'auto-continue', key: 'resumeAt' } as const, null)
7
8const clock = (ms: number) =>
9 new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
10
11export const register: Register = (on, options) => {
12 const threshold = Number(options.threshold)
13 const graceMs = Number(options.graceMinutes) * 60_000
14 const message = String(options.message)
15 let timer: { cancel: () => void } | undefined
16
17 const stopTimer = () => {
18 timer?.cancel()
19 timer = undefined
20 }
21
22 on('session.start', async ($, e, next) => {
23 await $.command.register({
24 name: 'auto-continue',
25 description: 'Resume automatically after the 5-hour limit resets',
26 argumentHint: '[on|off|status]',
27 immediate: true,
28 })
29
30 return next(e)
31 })
32
33 on('command.run', { command: 'auto-continue' }, async ($, e) => {
34 const arg = e.args.trim()
35 const was = await read($, isArmed)
36 const now = arg === 'on' ? true : arg === 'off' ? false : arg === '' ? !was : was
37 if (!now) {
38 stopTimer()
39 await update($, resumeAt, () => null)
40 }
41 await update($, isArmed, () => now)
42 const at = await read($, resumeAt)
43 const wait = at ? `, resuming ~${clock(at)}` : ''
44
45 return { text: `Auto-continue ${now ? 'ON' : 'OFF'}${wait}.` }
46 })
47
48 on('session.measure', async ($, e, next) => {
49 const w = e.rateLimits.find(l => l.kind === 'five_hour')
50 await update($, percent, () => w?.percentUsed ?? null)
51
52 return next(e)
53 })
54
55 on('turn.complete', async ($, e, next) => {
56 const done = await next(e)
57 if (e.reason !== 'error' || e.isAborted || !(await read($, isArmed))) {
58 return done
59 }
60
61 const { rateLimits } = await $.session.usage()
62 const w = rateLimits.find(l => l.kind === 'five_hour')
63 const reset = w?.resetsAt ? Date.parse(w.resetsAt) : NaN
64 const now = await $.clock.now()
65 // ponytail: an API error with the window >= 95% counts as the limit hit;
66 // if the error response leaves the usage stale, parse the reset time from the message instead
67 if (!w || w.percentUsed < 95 || !(reset > now)) {
68 return done
69 }
70
71 stopTimer()
72 const at = reset + graceMs
73 await update($, resumeAt, () => at)
74 $.ui.toast(`Limit reached. Resuming at ${clock(at)}.`)
75 timer = $.clock.after(at - now, () => {
76 timer = undefined
77 void update($, resumeAt, () => null)
78 void $.prompt.submit({ text: message })
79 })
80
81 return done
82 })
83
84 // the person typing takes over from a pending resume
85 on('prompt.submit', async ($, e, next) => {
86 if (timer && e.origin.kind !== 'plugin') {
87 stopTimer()
88 await update($, resumeAt, () => null)
89 }
90
91 return next(e)
92 })
93
94 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
95 const pct = await read($, percent)
96 const armed = await read($, isArmed)
97 const at = await read($, resumeAt)
98 const isQuiet = e.props.hasSurvey || (!armed && (pct === null || pct < threshold))
99 if (isQuiet) {
100 return next(e)
101 }
102
103 const { Box, Button, Text } = $.ui.resolve(e)
104
105 return (
106 <Box>
107 <Button
108 key="toggle"
109 label={`Auto-continue: ${armed ? 'ON' : 'OFF'}`}
110 onPress={async () => {
111 if (armed) {
112 stopTimer()
113 await update($, resumeAt, () => null)
114 }
115 await update($, isArmed, () => !armed)
116 }}
117 />
118 <Text dimColor>
119 {' '}
120 {at ? `resumes ~${clock(at)}` : `${pct ?? 0}% of 5h used`}
121 </Text>
122 </Box>
123 )
124 })
125}
126types/index.d.ts 12 lines1export type Timestamp = number | null
2
3declare module 'claude-code' {
4 interface PluginState {
5 'auto-continue': {
6 percent: number | null
7 isArmed: boolean
8 resumeAt: Timestamp
9 }
10 }
11}
12