Skip to content

fix: preserve error context in CLI failure exceptions#961

Open
itxaiohanglover wants to merge 1 commit into
anthropics:mainfrom
itxaiohanglover:fix/798-preserve-error-context
Open

fix: preserve error context in CLI failure exceptions#961
itxaiohanglover wants to merge 1 commit into
anthropics:mainfrom
itxaiohanglover:fix/798-preserve-error-context

Conversation

@itxaiohanglover
Copy link
Copy Markdown

Summary

When the Claude CLI subprocess exits non-zero, the SDK was destroying error context through a 3-step exception flattening chain, making failures impossible to diagnose:

  1. subprocess_cli.pyProcessError used hardcoded "Check stderr output for details" instead of actual CLI stderr
  2. query.py — Exceptions stringified via str(e), losing type and attributes (exit_code, stderr)
  3. query.pyreceive_messages() re-raised as bare Exception, so consumer except ClaudeSDKError handlers never fired

This PR fixes all three issues:

  • Stderr capture: SubprocessCLITransport now buffers a bounded tail (8KB) of stderr output and includes it in ProcessError.stderr instead of the placeholder string. Only active when stderr is already piped (callback registered), no behavior change for the non-piped path.
  • Exception preservation: The original exception object is passed through the message stream alongside the error text, so its type and attributes survive the round-trip.
  • Typed re-raise: receive_messages() now re-raises the preserved exception (or falls back to ClaudeSDKError), so consumer except ClaudeSDKError and except ProcessError handlers work correctly.

Changes

  • subprocess_cli.py: Add _stderr_tail deque buffer, capture stderr lines in _handle_stderr(), use captured tail in ProcessError instead of hardcoded string
  • query.py: Pass exception object in error message dict, re-raise typed exception in receive_messages()

Closes #798

Test plan

  • All 131 existing tests pass (test_query.py, test_transport.py, test_client.py)
  • ruff check passes
  • ruff format passes
  • mypy type checking passes

When the Claude CLI subprocess exits non-zero, the SDK was destroying
error context through exception flattening:

1. ProcessError used a hardcoded stderr placeholder instead of actual
   CLI stderr output
2. query.py stringified exceptions via str(e), losing type/attributes
3. receive_messages() re-raised as bare Exception, so consumer
   except ClaudeSDKError handlers never fired

Fix by:
- Capturing stderr tail in SubprocessCLITransport and using it in
  ProcessError instead of the placeholder
- Passing the original exception object through the message stream
- Re-raising the preserved exception (or ClaudeSDKError as fallback)
  in receive_messages() so typed exception handlers work correctly

Closes anthropics#798
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error context destroyed through exception flattening — CLI failures produce unhelpful 'Command failed with exit code 1'

1 participant