claude-code - 💡(How to fix) Fix telegram plugin: infinite CPU loop on EPIPE (broken pipe causes uncaughtException storm)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

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

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.

Code Example

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

---

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)

---

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`)
})

---

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 {}
})

---

process.stderr.on('error', (err: NodeJS.ErrnoException) => {
  if (err.code === 'EPIPE') shutdown()
})
RAW_BUFFERClick to expand / collapse

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

  1. Have Claude Code running with the Telegram plugin active
  2. Trigger a network event that breaks the stdio socket between bun server.ts and the parent Claude process (e.g. activate WireGuard with wg-quick up wg0, VPN reconnect, or network interface restart)
  3. 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-official v0.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()
})

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING