Skip to content

Commit 99d1371

Browse files
ashwin-antclaude
andauthored
feat: Enable real-time debug output via debug-to-stderr flag (#150)
- Add conditional stderr routing in subprocess transport - When debug-to-stderr flag is set, Claude CLI debug output goes directly to Python's stderr - Keeps stdout clean for JSON message parsing while providing debug visibility - Simplifies implementation by removing temp file and background task complexity - Update examples to demonstrate debug-to-stderr usage 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2c8c7fd commit 99d1371

File tree

2 files changed

+16
-50
lines changed

2 files changed

+16
-50
lines changed

src/claude_code_sdk/_internal/transport/subprocess_cli.py

Lines changed: 12 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
import logging
55
import os
66
import shutil
7-
import tempfile
8-
from collections import deque
97
from collections.abc import AsyncIterable, AsyncIterator
108
from contextlib import suppress
119
from pathlib import Path
@@ -42,9 +40,7 @@ def __init__(
4240
self._cwd = str(options.cwd) if options.cwd else None
4341
self._process: Process | None = None
4442
self._stdout_stream: TextReceiveStream | None = None
45-
self._stderr_stream: TextReceiveStream | None = None
4643
self._stdin_stream: TextSendStream | None = None
47-
self._stderr_file: Any = None # tempfile.NamedTemporaryFile
4844
self._ready = False
4945
self._exit_error: Exception | None = None # Track process exit errors
5046

@@ -174,12 +170,6 @@ async def connect(self) -> None:
174170

175171
cmd = self._build_command()
176172
try:
177-
# Create a temp file for stderr to avoid pipe buffer deadlock
178-
# We can't use context manager as we need it for the subprocess lifetime
179-
self._stderr_file = tempfile.NamedTemporaryFile( # noqa: SIM115
180-
mode="w+", prefix="claude_stderr_", suffix=".log", delete=False
181-
)
182-
183173
# Merge environment variables: system -> user -> SDK required
184174
process_env = {
185175
**os.environ,
@@ -190,11 +180,19 @@ async def connect(self) -> None:
190180
if self._cwd:
191181
process_env["PWD"] = self._cwd
192182

183+
# Only output stderr if customer explicitly requested debug output and provided a file object
184+
stderr_dest = (
185+
self._options.debug_stderr
186+
if "debug-to-stderr" in self._options.extra_args
187+
and self._options.debug_stderr
188+
else None
189+
)
190+
193191
self._process = await anyio.open_process(
194192
cmd,
195193
stdin=PIPE,
196194
stdout=PIPE,
197-
stderr=self._stderr_file,
195+
stderr=stderr_dest,
198196
cwd=self._cwd,
199197
env=process_env,
200198
)
@@ -234,7 +232,7 @@ async def close(self) -> None:
234232
if not self._process:
235233
return
236234

237-
# Close stdin first if it's still open
235+
# Close streams
238236
if self._stdin_stream:
239237
with suppress(Exception):
240238
await self._stdin_stream.aclose()
@@ -253,18 +251,8 @@ async def close(self) -> None:
253251
# Just try to wait, but don't block if it fails
254252
await self._process.wait()
255253

256-
# Clean up temp file
257-
if self._stderr_file:
258-
try:
259-
self._stderr_file.close()
260-
Path(self._stderr_file.name).unlink()
261-
except Exception:
262-
pass
263-
self._stderr_file = None
264-
265254
self._process = None
266255
self._stdout_stream = None
267-
self._stderr_stream = None
268256
self._stdin_stream = None
269257
self._exit_error = None
270258

@@ -358,46 +346,20 @@ async def _read_messages_impl(self) -> AsyncIterator[dict[str, Any]]:
358346
# Client disconnected
359347
pass
360348

361-
# Read stderr from temp file (keep only last N lines for memory efficiency)
362-
stderr_lines: deque[str] = deque(maxlen=100) # Keep last 100 lines
363-
if self._stderr_file:
364-
try:
365-
# Flush any pending writes
366-
self._stderr_file.flush()
367-
# Read from the beginning
368-
self._stderr_file.seek(0)
369-
for line in self._stderr_file:
370-
line_text = line.strip()
371-
if line_text:
372-
stderr_lines.append(line_text)
373-
except Exception:
374-
pass
375-
376349
# Check process completion and handle errors
377350
try:
378351
returncode = await self._process.wait()
379352
except Exception:
380353
returncode = -1
381354

382-
# Convert deque to string for error reporting
383-
stderr_output = "\n".join(list(stderr_lines)) if stderr_lines else ""
384-
if len(stderr_lines) == stderr_lines.maxlen:
385-
stderr_output = (
386-
f"[stderr truncated, showing last {stderr_lines.maxlen} lines]\n"
387-
+ stderr_output
388-
)
389-
390-
# Use exit code for error detection, not string matching
355+
# Use exit code for error detection
391356
if returncode is not None and returncode != 0:
392357
self._exit_error = ProcessError(
393358
f"Command failed with exit code {returncode}",
394359
exit_code=returncode,
395-
stderr=stderr_output,
360+
stderr="Check stderr output for details",
396361
)
397362
raise self._exit_error
398-
elif stderr_output:
399-
# Log stderr for debugging but don't fail on non-zero exit
400-
logger.debug(f"Process stderr: {stderr_output}")
401363

402364
def is_ready(self) -> bool:
403365
"""Check if transport is ready for communication."""

src/claude_code_sdk/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Type definitions for Claude SDK."""
22

3+
import sys
34
from collections.abc import Awaitable, Callable
45
from dataclasses import dataclass, field
56
from pathlib import Path
@@ -250,6 +251,9 @@ class ClaudeCodeOptions:
250251
extra_args: dict[str, str | None] = field(
251252
default_factory=dict
252253
) # Pass arbitrary CLI flags
254+
debug_stderr: Any = (
255+
sys.stderr
256+
) # File-like object for debug output when debug-to-stderr is set
253257

254258
# Tool permission callback
255259
can_use_tool: CanUseTool | None = None

0 commit comments

Comments
 (0)