telegram plugin: infinite CPU loop on EPIPE (broken pipe causes uncaughtException storm)
Bug description
When the MCP stdio transport socket between server.ts and its parent Claude Code process breaks (EPIPE), the uncaughtException handler fires and tries to write the error to process.stderr. If stderr is backed by the same broken socket, the write itself throws another EPIPE — which triggers uncaughtException again, infinitely.
Steps to reproduce
- Have Claude Code running with the Telegram plugin active
- Trigger a network event that breaks the stdio socket between
bun server.tsand the parent Claude process (e.g. activate WireGuard withwg-quick up wg0, VPN reconnect, or network interface restart) - The plugin process enters an infinite exception loop
Observed behavior
strace output (sampled over 3 seconds, pid=987370):
17,786 write() calls — ALL returning -1 EPIPE
98.5% of process CPU time in this loop
Repeated syscall pattern:
write(11, "telegram channel: uncaught exception: Error: EPIPE: broken pipe, write\n") = -1 EPIPE
--- SIGPIPE ---
write(11, "telegram channel: uncaught exception: Error: EPIPE: broken pipe, write\n") = -1 EPIPE
--- SIGPIPE ---
(thousands of times per second)
Multiple Claude Code sessions running in parallel → multiple plugin instances → compounding CPU usage (3 instances observed at ~60% CPU each).
Root cause
server.ts lines 73–78:
process.on('unhandledRejection', err => {
process.stderr.write(`telegram channel: unhandled rejection: ${err}\n`)
})
process.on('uncaughtException', err => {
process.stderr.write(`telegram channel: uncaught exception: ${err}\n`)
})
When err.code === 'EPIPE', writing to process.stderr (backed by the same broken socket) throws another EPIPE. The handler catches it and writes again — infinite loop.
The orphan watchdog (setInterval at lines 670–677, fires every 5s) should detect the broken pipe and call shutdown(), but it never gets to run because the synchronous exception loop starves the event loop entirely.
Environment
- OS: Linux 6.8.0 (Ubuntu), 1.9 GB RAM
- Plugin:
telegram@claude-plugins-officialv0.0.6 - Trigger:
wg-quick up wg0(WireGuard activation changed network routing, breaking the socket) - Sessions: multiple Claude Code sessions in tmux, each with its own plugin instance
Suggested fix
Guard against EPIPE in both error handlers and exit cleanly when the output pipe itself is broken:
process.on('unhandledRejection', err => {
if ((err as NodeJS.ErrnoException)?.code === 'EPIPE') { process.exit(0); return }
try { process.stderr.write(`telegram channel: unhandled rejection: ${err}\n`) } catch {}
})
process.on('uncaughtException', err => {
if ((err as NodeJS.ErrnoException)?.code === 'EPIPE') { process.exit(0); return }
try { process.stderr.write(`telegram channel: uncaught exception: ${err}\n`) } catch {}
})
Alternatively, listen for the error event on process.stderr:
process.stderr.on('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EPIPE') shutdown()
})This issue has 8 comments on GitHub. Read the full discussion on GitHub ↗