Denies the third consecutive poll or wait command when no edit has landed between them.

hooks/register.ts 63 lines1import type { Register } from 'claude-code'
2
3const LIMIT = 3
4const MIN_SLEEP_SECONDS = 10
5
6const WATCH =
7 /\bgh\s+run\s+(watch|view)\b|\bgh\s+pr\s+checks\b|\bvercel\s+(inspect|logs)\b|\buntil\b[^;]*;\s*do\b|\bwhile\b[^;]*;\s*do\b[\s\S]*\bsleep\b/
8
9export const isPoll = (command: string): boolean => {
10 if (WATCH.test(command)) {
11 return true
12 }
13
14 for (const match of command.matchAll(/\bsleep\s+(\d+(?:\.\d+)?)([smh]?)/g)) {
15 const scale = match[2] === 'h' ? 3600 : match[2] === 'm' ? 60 : 1
16
17 if (Number(match[1]) * scale >= MIN_SLEEP_SECONDS) {
18 return true
19 }
20 }
21
22 return false
23}
24
25export const register: Register = on => {
26 const polls = new Map<string, number>()
27
28 on('prompt.submit', ($, e, next) => {
29 polls.clear()
30
31 return next(e)
32 })
33
34 on('tool.call', ($, e, next) => {
35 const loop = e.agentId ?? 'main'
36
37 if (e.tool === 'Edit' || e.tool === 'Write' || e.tool === 'NotebookEdit') {
38 polls.delete(loop)
39
40 return next(e)
41 }
42
43 if (e.tool !== 'Bash' || !isPoll(e.command)) {
44 return next(e)
45 }
46
47 const count = (polls.get(loop) ?? 0) + 1
48
49 if (count < LIMIT) {
50 polls.set(loop, count)
51
52 return next(e)
53 }
54
55 polls.delete(loop)
56 $.ui.toast('watch-loop: held a third poll in a row')
57
58 return {
59 deny: `${$.plugin.name}: third poll or wait in a row with no edit between. Polling ships nothing. Do other work now and check once at the end of the expected duration, or say what is blocked.`,
60 }
61 })
62}
63