Skip to content

Commit 8ba69a9

Browse files
ashwin-antclaude
andcommitted
Add minimum Claude Code version check (2.0.0+)
- Add version check in subprocess transport that runs `claude -v` on connect - Display warning to stderr if version is below 2.0.0 - Update README prerequisites to specify Claude Code 2.0.0+ - Version check is non-blocking (warns but continues execution) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 2a9693e commit 8ba69a9

File tree

3 files changed

+98
-14
lines changed

3 files changed

+98
-14
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pip install claude-agent-sdk
1111
**Prerequisites:**
1212
- Python 3.10+
1313
- Node.js
14-
- Claude Code: `npm install -g @anthropic-ai/claude-code`
14+
- Claude Code 2.0.0+: `npm install -g @anthropic-ai/claude-code`
1515

1616
## Quick Start
1717

src/claude_agent_sdk/_internal/transport/subprocess_cli.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import json
44
import logging
55
import os
6+
import re
67
import shutil
8+
import sys
79
from collections.abc import AsyncIterable, AsyncIterator
810
from contextlib import suppress
911
from dataclasses import asdict
@@ -25,6 +27,7 @@
2527
logger = logging.getLogger(__name__)
2628

2729
_DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
30+
MINIMUM_CLAUDE_CODE_VERSION = "2.0.0"
2831

2932

3033
class SubprocessCLITransport(Transport):
@@ -202,6 +205,8 @@ async def connect(self) -> None:
202205
if self._process:
203206
return
204207

208+
await self._check_claude_version()
209+
205210
cmd = self._build_command()
206211
try:
207212
# Merge environment variables: system -> user -> SDK required
@@ -448,6 +453,46 @@ async def _read_messages_impl(self) -> AsyncIterator[dict[str, Any]]:
448453
)
449454
raise self._exit_error
450455

456+
async def _check_claude_version(self) -> None:
457+
"""Check Claude Code version and warn if below minimum."""
458+
version_process = None
459+
try:
460+
with anyio.fail_after(2): # 2 second timeout
461+
version_process = await anyio.open_process(
462+
[self._cli_path, "-v"],
463+
stdout=PIPE,
464+
stderr=PIPE,
465+
)
466+
467+
if version_process.stdout:
468+
stdout_bytes = await version_process.stdout.receive()
469+
version_output = stdout_bytes.decode().strip()
470+
471+
match = re.match(r"([0-9]+\.[0-9]+\.[0-9]+)", version_output)
472+
if match:
473+
version = match.group(1)
474+
version_parts = [int(x) for x in version.split(".")]
475+
min_parts = [
476+
int(x) for x in MINIMUM_CLAUDE_CODE_VERSION.split(".")
477+
]
478+
479+
if version_parts < min_parts:
480+
warning = (
481+
f"Warning: Claude Code version {version} is unsupported in the Agent SDK. "
482+
f"Minimum required version is {MINIMUM_CLAUDE_CODE_VERSION}. "
483+
"Some features may not work correctly."
484+
)
485+
logger.warning(warning)
486+
print(warning, file=sys.stderr)
487+
except Exception:
488+
pass
489+
finally:
490+
if version_process:
491+
with suppress(Exception):
492+
version_process.terminate()
493+
with suppress(Exception):
494+
await version_process.wait()
495+
451496
def is_ready(self) -> bool:
452497
"""Check if transport is ready for communication."""
453498
return self._ready

tests/test_transport.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,16 @@ def test_connect_close(self):
163163

164164
async def _test():
165165
with patch("anyio.open_process") as mock_exec:
166+
# Mock version check process
167+
mock_version_process = MagicMock()
168+
mock_version_process.stdout = MagicMock()
169+
mock_version_process.stdout.receive = AsyncMock(
170+
return_value=b"2.0.0 (Claude Code)"
171+
)
172+
mock_version_process.terminate = MagicMock()
173+
mock_version_process.wait = AsyncMock()
174+
175+
# Mock main process
166176
mock_process = MagicMock()
167177
mock_process.returncode = None
168178
mock_process.terminate = MagicMock()
@@ -175,7 +185,8 @@ async def _test():
175185
mock_stdin.aclose = AsyncMock()
176186
mock_process.stdin = mock_stdin
177187

178-
mock_exec.return_value = mock_process
188+
# Return version process first, then main process
189+
mock_exec.side_effect = [mock_version_process, mock_process]
179190

180191
transport = SubprocessCLITransport(
181192
prompt="test",
@@ -363,13 +374,25 @@ async def _test():
363374
with patch(
364375
"anyio.open_process", new_callable=AsyncMock
365376
) as mock_open_process:
377+
# Mock version check process
378+
mock_version_process = MagicMock()
379+
mock_version_process.stdout = MagicMock()
380+
mock_version_process.stdout.receive = AsyncMock(
381+
return_value=b"2.0.0 (Claude Code)"
382+
)
383+
mock_version_process.terminate = MagicMock()
384+
mock_version_process.wait = AsyncMock()
385+
386+
# Mock main process
366387
mock_process = MagicMock()
367388
mock_process.stdout = MagicMock()
368389
mock_stdin = MagicMock()
369390
mock_stdin.aclose = AsyncMock() # Add async aclose method
370391
mock_process.stdin = mock_stdin
371392
mock_process.returncode = None
372-
mock_open_process.return_value = mock_process
393+
394+
# Return version process first, then main process
395+
mock_open_process.side_effect = [mock_version_process, mock_process]
373396

374397
transport = SubprocessCLITransport(
375398
prompt="test",
@@ -379,11 +402,13 @@ async def _test():
379402

380403
await transport.connect()
381404

382-
# Verify open_process was called with correct env vars
383-
mock_open_process.assert_called_once()
384-
call_kwargs = mock_open_process.call_args.kwargs
385-
assert "env" in call_kwargs
386-
env_passed = call_kwargs["env"]
405+
# Verify open_process was called twice (version check + main process)
406+
assert mock_open_process.call_count == 2
407+
408+
# Check the second call (main process) for env vars
409+
second_call_kwargs = mock_open_process.call_args_list[1].kwargs
410+
assert "env" in second_call_kwargs
411+
env_passed = second_call_kwargs["env"]
387412

388413
# Check that custom env var was passed
389414
assert env_passed["MY_TEST_VAR"] == test_value
@@ -410,13 +435,25 @@ async def _test():
410435
with patch(
411436
"anyio.open_process", new_callable=AsyncMock
412437
) as mock_open_process:
438+
# Mock version check process
439+
mock_version_process = MagicMock()
440+
mock_version_process.stdout = MagicMock()
441+
mock_version_process.stdout.receive = AsyncMock(
442+
return_value=b"2.0.0 (Claude Code)"
443+
)
444+
mock_version_process.terminate = MagicMock()
445+
mock_version_process.wait = AsyncMock()
446+
447+
# Mock main process
413448
mock_process = MagicMock()
414449
mock_process.stdout = MagicMock()
415450
mock_stdin = MagicMock()
416451
mock_stdin.aclose = AsyncMock() # Add async aclose method
417452
mock_process.stdin = mock_stdin
418453
mock_process.returncode = None
419-
mock_open_process.return_value = mock_process
454+
455+
# Return version process first, then main process
456+
mock_open_process.side_effect = [mock_version_process, mock_process]
420457

421458
transport = SubprocessCLITransport(
422459
prompt="test",
@@ -426,11 +463,13 @@ async def _test():
426463

427464
await transport.connect()
428465

429-
# Verify open_process was called with correct user
430-
mock_open_process.assert_called_once()
431-
call_kwargs = mock_open_process.call_args.kwargs
432-
assert "user" in call_kwargs
433-
user_passed = call_kwargs["user"]
466+
# Verify open_process was called twice (version check + main process)
467+
assert mock_open_process.call_count == 2
468+
469+
# Check the second call (main process) for user
470+
second_call_kwargs = mock_open_process.call_args_list[1].kwargs
471+
assert "user" in second_call_kwargs
472+
user_passed = second_call_kwargs["user"]
434473

435474
# Check that user was passed
436475
assert user_passed == "claude"

0 commit comments

Comments
 (0)