Describe the bug
Every Copilot CLI session ends with Extension connection error: write EPIPE and Extension connection error: Cannot call write after a stream was destroyed — one pair per installed extension. This is a consistent, reproducible race condition: when a session terminates, the CLI sends SIGHUP to extension processes which immediately exit and destroy their stdio streams, but the CLI then attempts to invoke onSessionEnd hooks by writing to those already-destroyed streams.
A secondary variant also occurs at session startup: an Unhandled Rejection: Cannot call write after a stream was destroyed is logged when VS Code (IDE integration) disconnects within milliseconds of the session starting, before the CLI finishes initializing its transport layer.
Affected version
1.0.24
Steps to reproduce the behavior
- Install one or more extensions under ~/.copilot/extensions/
- Start a Copilot CLI session (e.g. via VS Code integration)
- End the session normally (Ctrl+D or closing the panel)
- Open ~/.copilot/logs/process-.log
- Observe the following pattern at the end of every log:
[INFO] Stopping extension: ~/.copilot/extensions/.mjs
[INFO] Unregistering foreground session:
[ERROR] Extension connection error: write EPIPE
[ERROR] Extension connection error: write EPIPE
[ERROR] Extension connection error: Cannot call write after a stream was destroyed
[ERROR] Extension connection error: Cannot call write after a stream was destroyed
The error count scales with the number of installed extensions (2 extensions = 4 errors).
Expected behavior
The CLI should not attempt to write to an extension's stdio stream after sending SIGHUP to that extension process. The
onSessionEnd hook delivery should either: (a) complete before SIGHUP is sent, or (b) gracefully no-op if the extension
connection is already disposed, without logging unhandled errors.
Additional context
- Node.js: v24.11.1
- OS: macOS
- Error is present in every session log across the full log history (~25 logs)
- Stack trace from the startup Unhandled Rejection variant points to app.js:4424 — the MCP/transport layer:
Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed
at _write (node:internal/streams/writable:489:11)
at Writable.write (node:internal/streams/writable:508:10)
at app.js:4424:14440 ← ldr.write / vft.doWrite
- The session-end variant produces Hook sessionEnd failed: Pending response rejected since connection got disposed in
follow-up log lines, confirming the hook fires after the connection is gone.
- Sessions function normally despite these errors — this is not a crash, but the Unhandled Rejection form is a more severe
unhandled promise rejection that should be caught internally.
Describe the bug
Every Copilot CLI session ends with Extension connection error: write EPIPE and Extension connection error: Cannot call write after a stream was destroyed — one pair per installed extension. This is a consistent, reproducible race condition: when a session terminates, the CLI sends SIGHUP to extension processes which immediately exit and destroy their stdio streams, but the CLI then attempts to invoke onSessionEnd hooks by writing to those already-destroyed streams.
A secondary variant also occurs at session startup: an Unhandled Rejection: Cannot call write after a stream was destroyed is logged when VS Code (IDE integration) disconnects within milliseconds of the session starting, before the CLI finishes initializing its transport layer.
Affected version
1.0.24
Steps to reproduce the behavior
[INFO] Stopping extension: ~/.copilot/extensions/.mjs
[INFO] Unregistering foreground session:
[ERROR] Extension connection error: write EPIPE
[ERROR] Extension connection error: write EPIPE
[ERROR] Extension connection error: Cannot call write after a stream was destroyed
[ERROR] Extension connection error: Cannot call write after a stream was destroyed
The error count scales with the number of installed extensions (2 extensions = 4 errors).
Expected behavior
The CLI should not attempt to write to an extension's stdio stream after sending SIGHUP to that extension process. The
onSessionEnd hook delivery should either: (a) complete before SIGHUP is sent, or (b) gracefully no-op if the extension
connection is already disposed, without logging unhandled errors.
Additional context
Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed
at _write (node:internal/streams/writable:489:11)
at Writable.write (node:internal/streams/writable:508:10)
at app.js:4424:14440 ← ldr.write / vft.doWrite
follow-up log lines, confirming the hook fires after the connection is gone.
unhandled promise rejection that should be caught internally.