anthropics / anthropics/claude-agent-sdk-python

ClaudeSDKClient can leak transports on double connect and pre-Query failures

Open
#1,161 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
8.1k
Forks
1.3k
PR merge metrics
PR metrics pending

Description

### Summary

Transport ownership is lost in two related connection lifecycle paths:

1. Calling `ClaudeSDKClient.connect()` twice overwrites the first live `Query` and transport without closing them.
2. An exception after `transport.connect()` but before a `Query` is assigned causes cleanup to discard the transport reference without closing it.

### Affected code

- `src/claude_agent_sdk/client.py:100-145`
- `src/claude_agent_sdk/client.py:187-229`
- `src/claude_agent_sdk/client.py:612-621`
- `src/claude_agent_sdk/_internal/client.py:127-174`
- `tests/test_streaming_client.py:1123-1141`

### Current behavior

`connect()` has no already-connected guard. A second call creates a new transport and assigns a new `Query`. The existing test only asserts that two transports were created; it does not assert that the first was closed.

For failures before `self._query = Query(...)`, the outer exception handler calls `disconnect()`. However, `disconnect()` only closes through `self._query`; when that is still `None`, it sets `self._transport = None` without calling `close()`.

One reachable example is an invalid `CLAUDE_CODE_STREAM_CLOSE_TIMEOUT`: integer conversion occurs after the subprocess connects but before Query construction.

The one-shot InternalClient has a similar ownership gap because `await chosen_transport.connect()` occurs before the `try/finally` that closes `query`.

### Why this matters

The abandoned transport can retain a CLI subprocess, stdin/stdout pipes, and background read tasks. Repeated failures or reconnect attempts can accumulate orphan processes and active sessions.

### Expected behavior

Every successfully or partially connected transport must have exactly one owner and must be closed on replacement or failure.

### Possible fix

- Reject a second `connect()` with `CLIConnectionError`, or explicitly disconnect the old connection before replacing it.
- Wrap the period between transport creation/connect and Query ownership transfer in a transport-level `try/finally`.
- Add assertions that the first transport is closed on double connect and that a connected transport is closed when setup fails before Query construction.

PR #1082 mentions the pre-Query transport gap as deliberately out of scope, but I could not find a dedicated issue or active fix for it.

### Environment

- Repository revision: current `main` audit at SDK version 0.2.128
- Bundled CLI version: 2.1.220
- Python test suite: 1,291 passed, 5 skipped
- Ruff and mypy: clean

I searched the existing issues and pull requests using the affected symbols and behavior before filing this.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.