From 95642de75b3be4e42bf72089c386f1339bacb603 Mon Sep 17 00:00:00 2001 From: peter-luminova Date: Fri, 27 Feb 2026 18:04:53 +0530 Subject: [PATCH 1/6] feat(windows): add helper to detect console-less parent process ROOT CAUSE: Windows GUI apps (no console) spawn child processes with visible console windows by default. CHANGES: - Add _should_suppress_console_window() helper function - Uses GetConsoleWindow() to detect parent console status - Returns True only on Windows when parent has no console IMPACT: Foundation for passing CREATE_NO_WINDOW flag to subprocess calls. FILES MODIFIED: - src/claude_agent_sdk/_internal/transport/subprocess_cli.py Co-Authored-By: Claude Sonnet 4.6 --- .../_internal/transport/subprocess_cli.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index 1f0aac58..f9cebf13 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -30,6 +30,24 @@ MINIMUM_CLAUDE_CODE_VERSION = "2.0.0" +def _should_suppress_console_window() -> bool: + """Check if we should suppress console window for subprocesses on Windows. + + Returns True when running on Windows and the parent process has no console + attached (e.g., GUI app, PyInstaller bundle with console=False). + + Returns: + bool: True if CREATE_NO_WINDOW should be used, False otherwise. + """ + if sys.platform != "win32": + return False + + import ctypes + + # GetConsoleWindow() returns NULL (0) if no console is attached + return not ctypes.windll.kernel32.GetConsoleWindow() + + class SubprocessCLITransport(Transport): """Subprocess transport using Claude Code CLI.""" From 44058821788b8fa85f420ae73c795b15ed9c399c Mon Sep 17 00:00:00 2001 From: peter-luminova Date: Fri, 27 Feb 2026 18:05:36 +0530 Subject: [PATCH 2/6] fix(windows): suppress console window in main agent subprocess ROOT CAUSE: anyio.open_process() in connect() didn't pass CREATE_NO_WINDOW, causing visible console windows on Windows GUI apps. CHANGES: - Convert inline kwargs to dict format - Conditionally add creationflags on Windows when parent has no console - Uses _should_suppress_console_window() helper IMPACT: Windows GUI apps (PyInstaller console=False) no longer show flashing console windows when creating agent sessions. FILES MODIFIED: - src/claude_agent_sdk/_internal/transport/subprocess_cli.py Co-Authored-By: Claude Sonnet 4.6 --- .../_internal/transport/subprocess_cli.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index f9cebf13..a26a4654 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -384,14 +384,25 @@ async def connect(self) -> None: # For backward compat: use debug_stderr file object if no callback and debug is on stderr_dest = PIPE if should_pipe_stderr else None + # Build kwargs dict for anyio.open_process + kwargs = { + "stdin": PIPE, + "stdout": PIPE, + "stderr": stderr_dest, + "cwd": self._cwd, + "env": process_env, + "user": self._options.user, + } + + # Windows: suppress console window if parent has no console + if _should_suppress_console_window(): + import subprocess + + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + self._process = await anyio.open_process( cmd, - stdin=PIPE, - stdout=PIPE, - stderr=stderr_dest, - cwd=self._cwd, - env=process_env, - user=self._options.user, + **kwargs, ) if self._process.stdout: From f82970ba1ad3a9b89d3469bea06c3d0bc1aabf6b Mon Sep 17 00:00:00 2001 From: peter-luminova Date: Fri, 27 Feb 2026 18:05:55 +0530 Subject: [PATCH 3/6] fix(windows): suppress console window in version check subprocess ROOT CAUSE: _check_claude_version() didn't pass CREATE_NO_WINDOW, causing console window flash during version check. CHANGES: - Convert inline kwargs to dict format - Conditionally add creationflags on Windows when parent has no console - Uses _should_suppress_console_window() helper IMPACT: Windows GUI apps no longer show console flash during version check. FILES MODIFIED: - src/claude_agent_sdk/_internal/transport/subprocess_cli.py Co-Authored-By: Claude Sonnet 4.6 --- .../_internal/transport/subprocess_cli.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index a26a4654..9244d62d 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -618,10 +618,21 @@ async def _check_claude_version(self) -> None: version_process = None try: with anyio.fail_after(2): # 2 second timeout + # Build kwargs for anyio.open_process + kwargs = { + "stdout": PIPE, + "stderr": PIPE, + } + + # Windows: suppress console window if parent has no console + if _should_suppress_console_window(): + import subprocess + + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + version_process = await anyio.open_process( [self._cli_path, "-v"], - stdout=PIPE, - stderr=PIPE, + **kwargs, ) if version_process.stdout: From b0fdf4e0ba6a23c8de2fb1c95a9f636ca390015c Mon Sep 17 00:00:00 2001 From: peter-luminova Date: Fri, 27 Feb 2026 18:09:16 +0530 Subject: [PATCH 4/6] test(windows): add unit tests for console detection helper ROOT CAUSE: No tests for _should_suppress_console_window() helper function. CHANGES: - Add TestShouldSuppressConsoleWindow test class - Test non-Windows platforms return False - Test Windows with/without console using mocks - Uses pytest.mark.skipif for Windows-specific tests IMPACT: Ensures console detection logic works correctly across platforms. FILES MODIFIED: - tests/test_windows_console.py (new) Co-Authored-By: Claude Sonnet 4.6 --- tests/test_windows_console.py | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/test_windows_console.py diff --git a/tests/test_windows_console.py b/tests/test_windows_console.py new file mode 100644 index 00000000..2eec2fb8 --- /dev/null +++ b/tests/test_windows_console.py @@ -0,0 +1,44 @@ +"""Tests for Windows console window suppression.""" + +import sys +from unittest.mock import patch + +import pytest + +from claude_agent_sdk._internal.transport.subprocess_cli import ( + _should_suppress_console_window, +) + + +class TestShouldSuppressConsoleWindow: + """Tests for _should_suppress_console_window helper.""" + + def test_returns_false_on_non_windows(self): + """Should return False on non-Windows platforms.""" + with patch("sys.platform", "linux"): + result = _should_suppress_console_window() + assert result is False + + def test_returns_false_on_non_windows_macos(self): + """Should return False on macOS.""" + with patch("sys.platform", "darwin"): + result = _should_suppress_console_window() + assert result is False + + @pytest.mark.skipif(sys.platform != "win32", reason="Windows-only test") + def test_returns_true_when_no_console_on_windows(self): + """Should return True on Windows when GetConsoleWindow returns 0.""" + # Mock ctypes to simulate no console + with patch("ctypes.windll.kernel32.GetConsoleWindow") as mock_get_console: + mock_get_console.return_value = 0 # No console + result = _should_suppress_console_window() + assert result is True + + @pytest.mark.skipif(sys.platform != "win32", reason="Windows-only test") + def test_returns_false_when_console_exists_on_windows(self): + """Should return False on Windows when GetConsoleWindow returns non-zero.""" + # Mock ctypes to simulate console exists + with patch("ctypes.windll.kernel32.GetConsoleWindow") as mock_get_console: + mock_get_console.return_value = 12345 # Console handle exists + result = _should_suppress_console_window() + assert result is False From cd893d85620cabc9db46e0eacf99c47adbca57e6 Mon Sep 17 00:00:00 2001 From: peter-luminova Date: Fri, 27 Feb 2026 18:10:22 +0530 Subject: [PATCH 5/6] test(windows): add integration tests for creationflags ROOT CAUSE: Need to verify CREATE_NO_WINDOW is actually passed to subprocesses. CHANGES: - Add TestWindowsSubprocessCreation integration tests - Test connect() passes creationflags when no console - Test _check_claude_version() passes creationflags when no console - Mock _should_suppress_console_window to simulate GUI app IMPACT: Confirms the fix works end-to-end on Windows. FILES MODIFIED: - tests/test_windows_integration.py (new) Co-Authored-By: Claude Sonnet 4.6 --- tests/test_windows_integration.py | 89 +++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/test_windows_integration.py diff --git a/tests/test_windows_integration.py b/tests/test_windows_integration.py new file mode 100644 index 00000000..40035f09 --- /dev/null +++ b/tests/test_windows_integration.py @@ -0,0 +1,89 @@ +"""Integration tests for Windows subprocess creation flags.""" + +import subprocess +import sys +from contextlib import suppress +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows-only test") +class TestWindowsSubprocessCreation: + """Tests that CREATE_NO_WINDOW is passed on Windows GUI apps.""" + + async def test_connect_passes_creationflags_when_no_console(self, mock_claude_path): + """Test that connect() passes CREATE_NO_WINDOW when parent has no console.""" + from claude_agent_sdk import ClaudeSDKClient + from claude_agent_sdk.types import ClaudeAgentOptions + + # Mock no console attached and anyio.open_process to capture kwargs + with ( + patch( + "claude_agent_sdk._internal.transport.subprocess_cli._should_suppress_console_window", + return_value=True, + ), + patch( + "claude_agent_sdk._internal.transport.subprocess_cli.anyio.open_process" + ) as mock_open_process, + ): + # Setup mock process + mock_process = AsyncMock() + mock_process.stdout = None + mock_process.stderr = None + mock_process.stdin = None + mock_open_process.return_value = mock_process + + # Create client and connect + options = ClaudeAgentOptions(cli_path=mock_claude_path) + client = ClaudeSDKClient(options) + client._transport._cli_path = mock_claude_path + + with suppress(Exception): + await client._transport.connect() + + # Verify creationflags was passed + assert mock_open_process.called + call_kwargs = mock_open_process.call_args[1] + assert "creationflags" in call_kwargs + assert call_kwargs["creationflags"] == subprocess.CREATE_NO_WINDOW + + async def test_version_check_passes_creationflags_when_no_console( + self, mock_claude_path + ): + """Test that version check passes CREATE_NO_WINDOW when parent has no console.""" + from claude_agent_sdk import ClaudeSDKClient + from claude_agent_sdk.types import ClaudeAgentOptions + + # Mock no console attached and anyio.open_process to capture kwargs + with ( + patch( + "claude_agent_sdk._internal.transport.subprocess_cli._should_suppress_console_window", + return_value=True, + ), + patch( + "claude_agent_sdk._internal.transport.subprocess_cli.anyio.open_process" + ) as mock_open_process, + ): + # Setup mock process with version output + mock_process = AsyncMock() + mock_stdout = AsyncMock() + mock_stdout.receive = AsyncMock(return_value=b"2.1.61\n") + mock_process.stdout = mock_stdout + mock_process.terminate = MagicMock() + mock_process.wait = AsyncMock() + mock_open_process.return_value = mock_process + + # Create client and trigger version check + options = ClaudeAgentOptions(cli_path=mock_claude_path) + client = ClaudeSDKClient(options) + client._transport._cli_path = mock_claude_path + + with suppress(Exception): + await client._transport._check_claude_version() + + # Verify creationflags was passed + assert mock_open_process.called + call_kwargs = mock_open_process.call_args[1] + assert "creationflags" in call_kwargs + assert call_kwargs["creationflags"] == subprocess.CREATE_NO_WINDOW From 157c35c4d3813b114890e70ea0ff3d026387df84 Mon Sep 17 00:00:00 2001 From: peter-luminova Date: Fri, 27 Feb 2026 18:12:00 +0530 Subject: [PATCH 6/6] chore: fix type annotations for windows console fix ROOT CAUSE: pyright type checking issues after adding console detection. CHANGES: - Add type: ignore comments for CREATE_NO_WINDOW (Windows-only constant) - Add type: ignore comments for **kwargs expansion (pyright false positive) IMPACT: Passes static type checking. FILES MODIFIED: - src/claude_agent_sdk/_internal/transport/subprocess_cli.py Co-Authored-By: Claude Sonnet 4.6 --- .../_internal/transport/subprocess_cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index 9244d62d..99736cd8 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -398,11 +398,11 @@ async def connect(self) -> None: if _should_suppress_console_window(): import subprocess - kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined] self._process = await anyio.open_process( cmd, - **kwargs, + **kwargs, # type: ignore[arg-type] ) if self._process.stdout: @@ -628,11 +628,11 @@ async def _check_claude_version(self) -> None: if _should_suppress_console_window(): import subprocess - kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined] version_process = await anyio.open_process( [self._cli_path, "-v"], - **kwargs, + **kwargs, # type: ignore[arg-type] ) if version_process.stdout: