SDK version
claude-agent-sdk==0.1.56
Description
When the Claude CLI subprocess exits with a non-zero code, the SDK destroys all error context through a 3-step chain, making failures impossible to diagnose:
_internal/transport/subprocess_cli.py:614 — ProcessError is created with stderr="Check stderr output for details" (hardcoded string, not actual stderr from the process)
_internal/query.py:250 — Caught in _read_task, flattened via str(e), sent as {"type": "error", "error": str(e)} through the message stream
_internal/query.py:726 — receive_messages() re-raises as bare Exception(message.get("error")), losing the ProcessError type entirely
Consumer code that catches ClaudeSDKError never fires because the exception arrives as bare Exception.
Reproduction
Any scenario where the Claude CLI process exits with code 1 (permission denied, internal error, bad config). The consumer receives:
Exception: Command failed with exit code 1 (exit code: 1)
Error output: Check stderr output for details
No actual stderr, no typed exception, no actionable detail.
Issues found (in order of severity)
| File |
Line |
Problem |
subprocess_cli.py |
616 |
stderr="Check stderr output for details" is hardcoded — actual stderr is never captured into the error |
query.py |
250 |
str(e) flattens ProcessError (which has exit_code/stderr attrs) into a plain string |
query.py |
726 |
raise Exception(...) instead of preserving/re-raising original exception type — except ClaudeSDKError in consumer code never triggers |
query.py |
207-209 |
Control request errors also create bare Exception from dict |
subprocess_cli.py |
446-449 |
stderr reading exceptions silently swallowed (except Exception: pass) |
subprocess_cli.py |
608 |
process.wait() failure caught as returncode = -1, no logging |
query.py |
272, 318, 334, 346, 385, 420 |
Bare Exception() instead of typed ClaudeSDKError subclasses |
Expected behavior
ProcessError.stderr should contain the actual stderr output from the CLI process, not a hardcoded string
query.py:726 should raise ProcessError (or at minimum ClaudeSDKError), not bare Exception — so consumer except ClaudeSDKError blocks work
query.py:250 should preserve the exception object (or at least its type and attributes) rather than stringifying it
Workaround
Consumer code must catch Exception (not ClaudeSDKError) and try getattr(exc, "exit_code", None) defensively. But even that doesn't help here because the attributes are stripped at step 2 before re-raising.
Environment
- Python 3.14.3
- claude-agent-sdk 0.1.56
- Windows 11
- Claude Code CLI installed via npm
SDK version
claude-agent-sdk==0.1.56Description
When the Claude CLI subprocess exits with a non-zero code, the SDK destroys all error context through a 3-step chain, making failures impossible to diagnose:
_internal/transport/subprocess_cli.py:614—ProcessErroris created withstderr="Check stderr output for details"(hardcoded string, not actual stderr from the process)_internal/query.py:250— Caught in_read_task, flattened viastr(e), sent as{"type": "error", "error": str(e)}through the message stream_internal/query.py:726—receive_messages()re-raises as bareException(message.get("error")), losing theProcessErrortype entirelyConsumer code that catches
ClaudeSDKErrornever fires because the exception arrives as bareException.Reproduction
Any scenario where the Claude CLI process exits with code 1 (permission denied, internal error, bad config). The consumer receives:
No actual stderr, no typed exception, no actionable detail.
Issues found (in order of severity)
subprocess_cli.pystderr="Check stderr output for details"is hardcoded — actual stderr is never captured into the errorquery.pystr(e)flattensProcessError(which hasexit_code/stderrattrs) into a plain stringquery.pyraise Exception(...)instead of preserving/re-raising original exception type —except ClaudeSDKErrorin consumer code never triggersquery.pyExceptionfrom dictsubprocess_cli.pyexcept Exception: pass)subprocess_cli.pyprocess.wait()failure caught asreturncode = -1, no loggingquery.pyException()instead of typedClaudeSDKErrorsubclassesExpected behavior
ProcessError.stderrshould contain the actual stderr output from the CLI process, not a hardcoded stringquery.py:726should raiseProcessError(or at minimumClaudeSDKError), not bareException— so consumerexcept ClaudeSDKErrorblocks workquery.py:250should preserve the exception object (or at least its type and attributes) rather than stringifying itWorkaround
Consumer code must catch
Exception(notClaudeSDKError) and trygetattr(exc, "exit_code", None)defensively. But even that doesn't help here because the attributes are stripped at step 2 before re-raising.Environment