fix: prevent stdout drain listener accumulation in long-running sessions#1
Open
fix: prevent stdout drain listener accumulation in long-running sessions#1
Conversation
…ectory traversal When a file doesn't exist yet (e.g., write operations), fs.realpath() fails with ENOENT and the code fell back to the unresolved path for the allowedDirectories check. An attacker could create a symlink inside an allowed directory pointing to a restricted location, then write a new file through that symlink — the path appeared to be inside the allowed directory but actually targeted a restricted one. The fix resolves the parent directory (and walks up to the deepest existing ancestor if needed) so symlinks anywhere in the path chain are followed to their real targets before the directory allowlist check runs. Closes wonderwhy-er#219 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ccumulation Fixes wonderwhy-er#391 — during long sessions with heavy tool usage, the Node.js MaxListenersExceededWarning fires on process.stdout because multiple concurrent MCP response writes each register an `once('drain', ...)` listener while the stdout buffer is backed up, exceeding the default limit of 10. Root cause: `sendLogNotification`, `sendProgress`, and `sendCustomNotification` all called `originalStdoutWrite` directly in fire-and-forget fashion, without backpressure handling. Each call could fill the write buffer, causing subsequent SDK `send()` calls to pile up drain listeners. Fix: route all custom notification writes through a serialised write queue (`writeNotificationToStdout` / `flushNotificationQueue`) that registers at most one `once('drain', ...)` listener at a time. Also raise `process.stdout.setMaxListeners(50)` as defence-in-depth to accommodate the SDK's own legitimate concurrent drain listeners during heavy tool usage without false-positive warnings. No API changes. No effect on normal operation when stdout is not full.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes wonderwhy-er#391 — during extended Claude Desktop sessions with heavy tool usage (50+ tool calls), Desktop Commander emits
MaxListenersExceededWarningonprocess.stdoutand tool calls begin timing out.Root cause
sendLogNotification,sendLog,sendProgress, andsendCustomNotificationall wrote to stdout by callingoriginalStdoutWritedirectly in a fire-and-forget manner — no backpressure handling. When the stdout buffer fills up under heavy load:send()call registersprocess.stdout.once('drain', resolve)while waiting for the buffer to drainoriginalStdoutWritecalls keep adding data to the buffer, delaying drainsend()calls pending, Node.js emitsMaxListenersExceededWarning(default limit: 10)Fix
Serialised notification write queue (
writeNotificationToStdout/flushNotificationQueue): all notification/progress/custom writes now go through a queue that registers at most oneonce('drain', ...)listener at a time. When stdout drains, the queue flushes sequentially. This eliminates the listener pile-up while also preventing dropped notifications (previouslyoriginalStdoutWritereturningfalsesilently discarded the write).process.stdout.setMaxListeners(50)in the constructor: defence-in-depth for the SDK's own legitimate concurrent drain listeners during bursts of parallel tool responses.Changes
src/custom-stdio.ts:stdoutQueue: string[]andstdoutDraining: booleanfieldswriteNotificationToStdout(data)andflushNotificationQueue()methodsoriginalStdoutWrite.call(...)calls in notification methods withwriteNotificationToStdout(...)process.stdout.setMaxListeners(50)in constructorNo breaking changes
Normal write path for JSON-RPC responses (through
setupStdoutFiltering) is unchanged. The queue only affects the custom notification write path.Test plan
read_file,list_directory,start_process,edit_block)mcp-server-desktop-commander.logshows noMaxListenersExceededWarningdrainlistener count stays at 1 in logs (addprocess.stdout.listenerCount('drain')trace if needed)npx tsc --noEmitpasses with zero errors🤖 Generated with Claude Code