Compare commits

...

111 commits

Author SHA1 Message Date
GitHub Actions
3eb12c5a37 chore: bump bundled CLI version to 2.0.74
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Test / test-e2e-docker (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
2025-12-19 22:12:38 +00:00
GitHub Actions
57e8b6ecd5 chore: bump bundled CLI version to 2.0.73
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test-e2e-docker (push) Blocked by required conditions
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.10) (push) Waiting to run
2025-12-19 00:16:21 +00:00
Ashwin Bhat
04347495b8
fix: resolve YAML syntax error in create-release-tag workflow (#429)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e-docker (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Replace heredoc with echo statements to fix YAML parsing issue. The
unindented heredoc content was breaking out of the literal block scalar,
causing `---` to be interpreted as a YAML document separator.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 17:39:12 -08:00
github-actions[bot]
a3df944128
chore: release v0.1.18 (#428)
This PR updates the version to 0.1.18 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml` to 0.1.18
- Updated version in `src/claude_agent_sdk/_version.py` to 0.1.18
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.18/
- Bundled CLI version: 2.0.72
- Install with: `pip install claude-agent-sdk==0.1.18`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 17:18:44 -08:00
GitHub Actions
91e65b1927 chore: bump bundled CLI version to 2.0.72
Some checks are pending
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e-docker (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
2025-12-17 21:59:10 +00:00
GitHub Actions
27575ae2ca chore: bump bundled CLI version to 2.0.71
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e-docker (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
2025-12-16 22:09:36 +00:00
Ashwin Bhat
a0ce44a3fa
Add Docker-based test infrastructure for e2e tests (#424)
## Summary

- Add `Dockerfile.test`: Python 3.12 image with Claude Code CLI
installed
- Add `scripts/test-docker.sh`: Local script to run tests in Docker
- Add `test-e2e-docker` job to CI workflow that runs the full e2e suite
in a container
- Add `.dockerignore` to speed up Docker builds

## Context

This helps catch Docker-specific issues like #406 where filesystem-based
agents loaded via `setting_sources=["project"]` may silently fail in
Docker environments.

## Local Usage

```bash
# Run unit tests in Docker (no API key needed)
./scripts/test-docker.sh unit

# Run e2e tests in Docker
ANTHROPIC_API_KEY=sk-... ./scripts/test-docker.sh e2e

# Run all tests
ANTHROPIC_API_KEY=sk-... ./scripts/test-docker.sh all
```

## Test plan

- [x] Unit tests pass in Docker locally (129 passed)
- [ ] CI job runs successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 10:53:13 -08:00
Ashwin Bhat
904c2ec33c
chore: use CHANGELOG.md content for GitHub release notes (#420)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Replace auto-generated release notes with content extracted from
CHANGELOG.md for the specific version being released. This provides more
structured and consistent release notes with proper sections like Bug
Fixes, New Features, etc.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-15 16:53:23 -08:00
github-actions[bot]
eba5675328
chore: release v0.1.17 (#419)
This PR updates the version to 0.1.17 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml` to 0.1.17
- Updated version in `src/claude_agent_sdk/_version.py` to 0.1.17
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.17/
- Bundled CLI version: 2.0.70
- Install with: `pip install claude-agent-sdk==0.1.17`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-15 16:41:49 -08:00
GitHub Actions
5752f38834 chore: bump bundled CLI version to 2.0.70 2025-12-15 23:52:59 +00:00
Noah Zweben
0ae5c3285c
Add UUID to UserMessage response type to improve devX for rewind (#418)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
2025-12-15 09:03:58 -08:00
GitHub Actions
f834ba9e15 chore: bump bundled CLI version to 2.0.69
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
2025-12-13 01:00:44 +00:00
github-actions[bot]
a1c338726f
chore: release v0.1.16 (#412)
This PR updates the version to 0.1.16 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml` to 0.1.16
- Updated version in `src/claude_agent_sdk/_version.py` to 0.1.16
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.16/
- Bundled CLI version: 2.0.68
- Install with: `pip install claude-agent-sdk==0.1.16`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-12 16:26:36 -08:00
GitHub Actions
d2b3477a4e chore: bump bundled CLI version to 2.0.68 2025-12-12 23:32:48 +00:00
lif
3cbb9e56be
fix: parse error field in AssistantMessage to enable rate limit detection (#405)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
## Summary

Fixes #401

Enables applications to detect API errors (especially `rate_limit`
errors) by properly parsing the `error` field in `AssistantMessage`.

## Problem

The SDK defines `AssistantMessage.error` (including `"rate_limit"`), but
the message parser never extracted this field from the CLI response.
This made it impossible for applications to:
- Detect when rate limits are hit
- Implement retry logic
- Handle other API errors gracefully

## Solution

Added error field extraction in the message parser:

```python
return AssistantMessage(
    content=content_blocks,
    model=data["message"]["model"],
    parent_tool_use_id=data.get("parent_tool_use_id"),
    error=data["message"].get("error"),  # ← Now extracts error field
)
```

## Changes

**Modified: `src/claude_agent_sdk/_internal/message_parser.py`**

The parser now extracts the `error` field from API responses and
populates it in the `AssistantMessage` object.

## Usage Example

Applications can now detect and handle rate limits:

```python
async for message in client.receive_response():
    if isinstance(message, AssistantMessage):
        if message.error == "rate_limit":
            print("Rate limit hit! Implementing backoff...")
            await asyncio.sleep(60)
            # Retry logic here
        elif message.error:
            print(f"API error: {message.error}")
```

## Testing

-  Passed ruff linting and formatting
-  Passed mypy type checking
-  All existing tests pass

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)

## Impact

This fix enables production applications to:
- Implement proper error handling for API errors
- Build robust retry logic for rate limits
- Provide better user feedback when errors occur
- Avoid silent failures when the API returns errors

---

🤖 Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2025-12-11 10:55:01 -08:00
github-actions[bot]
5b912962e2
chore: release v0.1.15 (#408)
This PR updates the version to 0.1.15 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml` to 0.1.15
- Updated version in `src/claude_agent_sdk/_version.py` to 0.1.15
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.15/
- Bundled CLI version: 2.0.62
- Install with: `pip install claude-agent-sdk==0.1.15`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-11 10:53:45 -08:00
Noah Zweben
53482d8955
feat: add file checkpointing and rewind_files support (#395)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
## Summary

- Add `enable_file_checkpointing` option to `ClaudeAgentOptions`
- Add `rewind_files(user_message_id)` method to `ClaudeSDKClient` and
`Query`
- Add `SDKControlRewindFilesRequest` type for the control protocol
- Set `CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING` env var when enabled

This adds Python SDK support for the file rewind feature from
claude-cli-internal PR #11265.

## Test plan

- [x] Verified imports work correctly
- [x] Verified linting passes (`ruff check`)
- [x] Verified existing tests still pass (106 passed, pre-existing
failures unrelated to this change)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-12-09 10:16:03 -08:00
sarahdeaton
4acfcc2d39
Add license and terms section to README. (#399)
Add "License and terms" section to README clarifying that use of the SDK
is governed by Anthropic's Commercial Terms of Service
2025-12-09 09:33:42 -08:00
github-actions[bot]
b5447d999d
chore: release v0.1.14 (#398)
This PR updates the version to 0.1.14 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml` to 0.1.14
- Updated version in `src/claude_agent_sdk/_version.py` to 0.1.14
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.14/
- Bundled CLI version: 2.0.62
- Install with: `pip install claude-agent-sdk==0.1.14`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2025-12-09 08:22:21 -08:00
GitHub Actions
ccff8ddf48 chore: bump bundled CLI version to 2.0.62
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
2025-12-09 02:12:13 +00:00
GitHub Actions
db4a6f7c28 chore: bump bundled CLI version to 2.0.61
Some checks failed
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
2025-12-07 10:47:47 +00:00
Ashwin Bhat
5625286212
fix: move fetch-depth to publish job and use claude-opus-4-5 for changelog (#394)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
The fetch-depth: 0 was on build-wheels but changelog generation happens
in the publish job. Moved it to the correct location and upgraded the
model for better changelog generation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-06 16:52:09 -08:00
github-actions[bot]
cf6b85fc5d
chore: release v0.1.13 (#393)
Some checks failed
Test / test-examples (3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
This PR updates the version to 0.1.13 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml` to 0.1.13
- Updated version in `src/claude_agent_sdk/_version.py` to 0.1.13
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.13/
- Bundled CLI version: 2.0.59
- Install with: `pip install claude-agent-sdk==0.1.13`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-05 16:33:33 -08:00
GitHub Actions
00b5730be6 chore: bump bundled CLI version to 2.0.60 2025-12-06 00:10:43 +00:00
GitHub Actions
1b3e35d14e chore: bump bundled CLI version to 2.0.59
Some checks are pending
Lint / lint (push) Waiting to run
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
2025-12-04 23:09:33 +00:00
Ramazan Rakhmatullin
69a310cc3f
fix: propagate CLI errors to pending control requests (#388)
## Summary

When the CLI exits with an error (e.g., invalid session ID passed to
`--resume`), signal all pending control requests immediately instead of
waiting for the 60-second timeout.

**The fix adds 4 lines** to the exception handler in `_read_messages`:

```python
# Signal all pending control requests so they fail fast instead of timing out
for request_id, event in list(self.pending_control_responses.items()):
    if request_id not in self.pending_control_results:
        self.pending_control_results[request_id] = e
        event.set()
```

## Problem

When the CLI exits with an error, the SDK's message reader catches it
but doesn't notify pending control requests. This causes `initialize()`
to wait for the full 60-second timeout even though the error is known
within seconds.

Example scenario:
1. User passes invalid session ID via
`ClaudeAgentOptions(resume="invalid-id")`
2. CLI prints `No conversation found with session ID: xxx` and exits
with code 1
3. SDK message reader catches the error after ~3 seconds
4. But `initialize()` keeps waiting for 60 seconds before timing out

## Solution

The existing code at `_send_control_request` lines 361-362 already
handles exceptions in results:
```python
if isinstance(result, Exception):
    raise result
```

The fix simply signals all pending control events when an error occurs,
allowing them to fail fast with the actual error instead of timing out.

## Test Plan

- [ ] Test with invalid session ID - should fail fast (~3s) instead of
timing out (60s)
- [ ] Test normal flow still works
- [ ] Test multiple pending requests all get signaled

Fixes #387
2025-12-04 14:28:36 -08:00
梨梨果
6791efec93
fix: add McpServer runtime placeholder for Pydantic 2.12+ compatibility (#385)
## Summary
- Add runtime placeholder for `McpServer` type to fix Pydantic 2.12+
compatibility
- `McpServer` was only imported under `TYPE_CHECKING`, causing
`PydanticUserError` at runtime

Fixes #384

Co-authored-by: lyrica <lyrica@example.com>
2025-12-04 14:28:24 -08:00
Carlos Cuevas
2d67166cae
fix: add write lock to prevent concurrent transport writes (#391)
## TL;DR
Adds a write lock to `SubprocessCLITransport` to prevent concurrent
writes from parallel subagents.

---

## Overview
When multiple subagents run in parallel and invoke MCP tools, the CLI
sends concurrent `control_request` messages. Each handler tries to write
a response back to the subprocess stdin at the same time. Trio's
`TextSendStream` isn't thread-safe for concurrent access, so this causes
`BusyResourceError`.

This PR adds an `anyio.Lock` around all write operations (`write()`,
`end_input()`, and the stdin-closing part of `close()`). The lock
serializes concurrent writes so they happen one at a time. The `_ready`
flag is now set inside the lock during `close()` to prevent a TOCTOU
race where `write()` checks `_ready`, then `close()` sets it and closes
the stream before `write()` actually sends data.

---

## Call Flow
```mermaid
flowchart TD
    A["write()<br/>subprocess_cli.py:505"] --> B["acquire _write_lock<br/>subprocess_cli.py:507"]
    B --> C["check _ready & stream<br/>subprocess_cli.py:509"]
    C --> D["_stdin_stream.send()<br/>subprocess_cli.py:523"]
    
    E["close()<br/>subprocess_cli.py:458"] --> F["acquire _write_lock<br/>subprocess_cli.py:478"]
    F --> G["set _ready = False<br/>subprocess_cli.py:479"]
    G --> H["close _stdin_stream<br/>subprocess_cli.py:481"]
    
    I["end_input()<br/>subprocess_cli.py:531"] --> J["acquire _write_lock<br/>subprocess_cli.py:533"]
    J --> K["close _stdin_stream<br/>subprocess_cli.py:535"]
```
2025-12-04 14:27:01 -08:00
github-actions[bot]
00332f32dc
chore: release v0.1.12 (#392)
This PR updates the version to 0.1.12 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml` to 0.1.12
- Updated version in `src/claude_agent_sdk/_version.py` to 0.1.12
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.12/
- Bundled CLI version: 2.0.58
- Install with: `pip install claude-agent-sdk==0.1.12`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-12-04 10:30:12 -08:00
Ashwin Bhat
ea0ef25e71
feat: add tools option to ClaudeAgentOptions (#389)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Add support for the `tools` option matching the TypeScript SDK, which
controls the base set of available tools separately from
allowed/disallowed tool filtering.

Supports three modes:
- Array of tool names: `["Read", "Edit", "Bash"]`
- Empty array: `[]` (disables all built-in tools)
- Preset object: `{"type": "preset", "preset": "claude_code"}`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 09:27:02 -08:00
Ashwin Bhat
4e56cb12a9
feat: add SDK beta support with SdkBeta type and betas option (#390)
Port the SdkBeta type and betas option from the TypeScript SDK to enable
SDK users to pass beta feature flags (e.g., 1M context window) to the
CLI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 09:26:01 -08:00
GitHub Actions
243703531b chore: bump bundled CLI version to 2.0.58
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
2025-12-03 20:09:56 +00:00
github-actions[bot]
9809fb6b54
chore: release v0.1.11 (#383)
This PR updates the version to 0.1.11 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml` to 0.1.11
- Updated version in `src/claude_agent_sdk/_version.py` to 0.1.11
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.11/
- Bundled CLI version: 2.0.57
- Install with: `pip install claude-agent-sdk==0.1.11`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2025-12-03 11:42:09 -08:00
GitHub Actions
afa39dc1bf chore: bump bundled CLI version to 2.0.57
Some checks are pending
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
2025-12-03 05:24:28 +00:00
GitHub Actions
2a53aba5b9 chore: bump bundled CLI version to 2.0.56
Some checks are pending
Lint / lint (push) Waiting to run
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
2025-12-02 01:35:15 +00:00
John Ott
6e1769f8fd
feat: bundle claude code CLI for linux arm64 (#373)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Currently wheels with packaged claude-code CLI are only published for
windows amd64, linux x86_64 and macos arm64. For version 0.1.10, we can
see the following download files are available on pypi contain the
following artifacts:
https://pypi.org/project/claude-agent-sdk/0.1.10/#files

- claude_agent_sdk-0.1.10.tar.gz
- claude_agent_sdk-0.1.10-py3-none-win_amd64.whl
- claude_agent_sdk-0.1.10-py3-none-manylinux_2_17_x86_64.whl
- claude_agent_sdk-0.1.10-py3-none-macosx_11_0_arm64.whl

The existing publishing code should support adding a new linux arm64
wheel builder, using the Github ARM runners:
https://github.blog/changelog/2025-08-07-arm64-hosted-runners-for-public-repositories-are-now-generally-available/

Unfortunately, there's no `ubuntu-latest-arm` label similar to the one
we use for the other builds, so I'm using the `ubuntu-24.04-arm` label.
2025-12-01 12:44:57 -08:00
Dalton Flanagan
a2f24a3101
fix: wait for first result before closing stdin if SDK MCP present (#380)
Port SDK MCP fix from TypeScript to Python.

Now, when SDK MCP servers or hooks are present, stream_input() waits for
the first result message before closing stdin, allowing bidirectional
control protocol communication to complete.

Fixes repro in
https://github.com/anthropics/claude-agent-sdk-python/issues/266.

The `query()` design requires input streams to be held open by the user
for SDK MCP bidirectional communication to work. This has confused a lot
of folks, so we're moving towards a more explicit lifecycle design. In
the meantime, this is the way we've addressed it with V1 APIs in
https://github.com/anthropics/claude-agent-sdk-typescript.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 10:19:51 -08:00
Ashwin Bhat
49482e1dfd
fix: correct mypy type ignore error codes for MCP decorators (#379)
Update type: ignore comments to use `untyped-decorator` instead of
`misc` to match the actual mypy error codes reported in CI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 08:40:58 -08:00
ollie-anthropic
f21f63e181
Create sandbox adapter interface for Python SDK (#363)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Adds programmatic sandbox configuration to the Python SDK, matching the
TypeScript SDK's approach.

Changes:
- Add SandboxSettings, SandboxNetworkConfig, SandboxIgnoreViolations
types
- Add sandbox field to ClaudeAgentOptions
- Merge sandbox into --settings CLI flag in SubprocessCLITransport
- Export sandbox types from package __init__.py
- Add comprehensive tests for sandbox settings

**Important:** Filesystem and network restrictions are configured via
permission rules (Read/Edit/WebFetch), not via these sandbox settings.
The sandbox settings control sandbox behavior (enabled, auto-allow,
excluded commands, etc.).

Example usage:
```python
from claude_agent_sdk import query, SandboxSettings

result = query(
    prompt='Build and test the project',
    options=ClaudeAgentOptions(
        sandbox={
            'enabled': True,
            'autoAllowBashIfSandboxed': True,
            'excludedCommands': ['docker'],
            'network': {
                'allowLocalBinding': True,
                'allowUnixSockets': ['/var/run/docker.sock']
            }
        }
    )
)
```

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 01:44:59 -08:00
GitHub Actions
d553184ef6 chore: bump bundled CLI version to 2.0.55
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
2025-11-27 00:01:24 +00:00
github-actions[bot]
e2f8d814ea
chore: release v0.1.10 (#369)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
This PR updates the version to 0.1.10 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml` to 0.1.10
- Updated version in `src/claude_agent_sdk/_version.py` to 0.1.10
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.10/
- Bundled CLI version: 2.0.53
- Install with: `pip install claude-agent-sdk==0.1.10`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2025-11-25 12:58:54 -05:00
GitHub Actions
be915896af chore: bump bundled CLI version to 2.0.53
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
2025-11-25 01:55:19 +00:00
GitHub Actions
d15f26da91 chore: bump bundled CLI version to 2.0.52 2025-11-24 23:32:15 +00:00
GitHub Actions
112f3aa959 chore: bump bundled CLI version to 2.0.51
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
2025-11-24 18:48:28 +00:00
Noah Zweben
493f49fad9
Added slack issue notifier for new opened issues (#356)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Posts new issues to claude-agent-sdk-feedback. Can't use the default
github slack bot as it's too noisy (all comments and issue updates such
as comments, closing, etc...)

---------

Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-11-23 21:24:25 -08:00
Harutaka Kawamura
7a5b413159
Fix: Fix HookMatcher.timeout type to float (#357)
According to https://www.schemastore.org/claude-code-settings.json, the
`timeout` type is `number`, not `integer`

Signed-off-by: harupy <17039389+harupy@users.noreply.github.com>
2025-11-23 18:22:12 -05:00
GitHub Actions
23183a2698 chore: bump bundled CLI version to 2.0.50
Some checks failed
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
2025-11-21 23:16:27 +00:00
Michael Dworsky
41ceacd807
Use CLAUDE_CODE_STREAM_CLOSE_TIMEOUT (if present) to override initialize() timeout (#354) 2025-11-21 16:46:22 -05:00
github-actions[bot]
f446e3e42a
chore: release v0.1.9 (#353)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
This PR updates the version to 0.1.9 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml` to 0.1.9
- Updated version in `src/claude_agent_sdk/_version.py` to 0.1.9
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.9/
- Bundled CLI version: 2.0.49
- Install with: `pip install claude-agent-sdk==0.1.9`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2025-11-21 12:51:05 -08:00
GitHub Actions
36c75374ec chore: bump bundled CLI version to 2.0.49
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
2025-11-21 01:31:38 +00:00
shawnm-anthropic
b0fb5b082a
Add AssistantMessageError type (#352)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
This brings Python to parity with
https://github.com/anthropics/claude-cli-internal/pull/10358.
2025-11-19 19:23:10 -05:00
GitHub Actions
ab83878f5a chore: bump bundled CLI version to 2.0.47 2025-11-19 23:12:47 +00:00
GitHub Actions
179818235a chore: bump bundled CLI version to 2.0.46 2025-11-19 22:56:34 +00:00
Dickson Tsai
84edd73041
feat: add timeout parameter to HookMatcher (#351)
## Summary

- Adds optional `timeout` field to `HookMatcher` dataclass in `types.py`
that allows users to specify a custom timeout (in seconds) for hooks
- Propagates the timeout value through:
- `client.py` and `_internal/client.py`: `_convert_hooks_to_internal()`
method
  - `_internal/query.py`: hook config sent to CLI

## Test plan

- [x] Verify hooks work without timeout specified (default behavior)
- [x] Verify custom timeout is passed to CLI when specified in
HookMatcher

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-19 13:50:33 -08:00
Ashwin Bhat
bf528a1221
refactor: source CLI version from code in publish workflow (#350)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Remove the claude_code_version workflow input and instead read the CLI
version directly from src/claude_agent_sdk/_cli_version.py. This allows
the version to be managed separately and updated by automation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-19 09:49:35 -08:00
github-actions[bot]
d5dc615bd4
chore: release v0.1.8 (#346)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
This PR updates the version to 0.1.8 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml` to 0.1.8
- Updated version in `src/claude_agent_sdk/_version.py` to 0.1.8
- Updated bundled CLI version in `src/claude_agent_sdk/_cli_version.py`
to 2.0.45
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.8/
- Bundled CLI version: 2.0.45
- Install with: `pip install claude-agent-sdk==0.1.8`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-11-18 21:21:57 -08:00
Ashwin Bhat
bab98e717e
fix: install wheel package for retagging platform-specific wheels (#345)
The build_wheel.py script uses `python -m wheel tags` to retag wheels
with platform-specific tags, but `wheel` wasn't explicitly installed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-18 21:05:18 -08:00
Ashwin Bhat
ddc37c7330
fix: disable artifact compression to prevent wheel corruption (#344)
Wheels are already ZIP files - double compression via GitHub Actions
artifacts can cause "Mis-matched data size" errors on PyPI upload.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-18 20:55:17 -08:00
Ashwin Bhat
58cfffc623
fix: use PowerShell installer for Windows CLI download (#343)
The bash install script (install.sh) explicitly rejects Windows. Use the
PowerShell installer (install.ps1) instead when running on Windows,
matching the approach used in test.yml.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-18 20:47:26 -08:00
Ashwin Bhat
35dd5b4bcc
fix: remove emojis from build_wheel.py for Windows compatibility (#342)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Windows console encoding (cp1252) doesn't support Unicode emoji
characters, causing UnicodeEncodeError in CI. Replaced all emoji
characters with plain text equivalents.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-18 17:59:48 -08:00
Ashwin Bhat
ce99e9d2eb
feat: bundle Claude Code CLI in pip package (#283)
Bundle platform-specific Claude Code CLI binaries directly in the Python
package, eliminating the need for separate CLI installation.

## Changes

### Build System
- Created `scripts/download_cli.py` to fetch CLI during build
- Created `scripts/build_wheel.py` for building platform-specific wheels
- Created `scripts/update_cli_version.py` to track bundled CLI version
- Updated `pyproject.toml` to properly bundle CLI without duplicate file
warnings
- Made twine check non-blocking (License-File warnings are false
positives)

### Runtime
- Modified `subprocess_cli.py` to check for bundled CLI first
- Added `_cli_version.py` to track which CLI version is bundled
- SDK automatically uses bundled CLI, falling back to system
installation if not found
- Users can still override with `cli_path` option

### Release Workflow
- Updated GitHub workflow to build separate wheels per platform (macOS,
Linux, Windows)
- Workflow now accepts two inputs:
  - `version`: Package version to publish (e.g., `0.1.5`)
- `claude_code_version`: CLI version to bundle (e.g., `2.0.0` or
`latest`)
- Workflow builds platform-specific wheels with bundled CLI
- Creates release PR that updates:
  - `pyproject.toml` version
  - `src/claude_agent_sdk/_version.py`
  - `src/claude_agent_sdk/_cli_version.py` with bundled CLI version
  - `CHANGELOG.md` with auto-generated release notes

### Documentation
- Updated README to reflect bundled CLI (removed Node.js requirement)
- Added release workflow documentation
- Added local wheel building instructions

## Benefits

- **Zero external dependencies**: No need for Node.js or npm
- **Easier installation**: Single `pip install` gets everything
- **Version control**: Track exactly which CLI version is bundled
- **Flexible releases**: Can release new package versions with updated
CLI without code changes
- **Better user experience**: Works out of the box with no setup

Platform-specific wheels are automatically selected by pip during
installation based on the user's OS and architecture.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-18 17:21:15 -08:00
github-actions[bot]
6f209075bc
chore: release v0.1.7 (#341)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
This PR updates the version to 0.1.7 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_agent_sdk/_version.py`
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.7/
- Install with: `pip install claude-agent-sdk==0.1.7`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: inigo <bogini@users.noreply.github.com>
2025-11-18 11:13:45 -08:00
bogini
50d68409c6
feat: add structured output support (#340)
Add structured output support to Python SDK.

## Usage

```python
from claude_agent_sdk import query, ClaudeAgentOptions

schema = {
    "type": "object",
    "properties": {"count": {"type": "number"}},
    "required": ["count"]
}

async for msg in query(
    prompt="Count files in src/",
    options=ClaudeAgentOptions(
        output_format={"type": "json_schema", "schema": schema}
    )
):
    if hasattr(msg, 'structured_output'):
        print(msg.structured_output)
```

## Documentation

https://docs.claude.com/en/docs/agent-sdk/structured-outputs

## Tests

- Unit tests:
`tests/test_integration.py::TestIntegration::test_structured_output`
- E2E tests: `e2e-tests/test_structured_output.py` (4 tests)
2025-11-18 11:01:17 -08:00
Suzanne Wang
ff425b293d
Add fallback model handling for parity with TypeScript SDK (#317)
Some checks failed
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Add support for automatic model fallback when primary model is
overloaded. The Python SDK passes the fallback_model parameter to the
Claude CLI, which handles the validation and fallback logic.

Changes:
- Add fallback_model parameter to ClaudeAgentOptions
- Pass --fallback-model to CLI subprocess
- Add test for fallback model command building

The validation that fallback_model != model happens at the CLI layer,
keeping the SDK implementation simple and focused on parameter passing.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-07 11:25:45 -08:00
Ashwin Bhat
5a4cc2f41a
feat: add support for Claude CLI at ~/.claude/local/claude (#302)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Add ~/.claude/local/claude to the list of locations checked when finding
the Claude CLI binary.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-31 08:52:17 -07:00
github-actions[bot]
c30ffbeb56
chore: release v0.1.6 (#301)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
This PR updates the version to 0.1.6 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_agent_sdk/_version.py`
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.6/
- Install with: `pip install claude-agent-sdk==0.1.6`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-10-30 22:40:51 -07:00
yokomotod
841f8c0614
fix: uses empty system prompt by default (#290)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
fix #289
2025-10-30 22:03:42 -07:00
Ashwin Bhat
5256af2dac
Update publish.yml (#300) 2025-10-30 17:05:10 -07:00
Ashwin Bhat
edad138cb0
Limit CI examples to Python 3.13 only (#299)
Reduce CI job count by only running examples on Python 3.13 instead of
all Python versions (3.10-3.13). This reduces the combinatorial
explosion while still ensuring examples work on the latest Python
version.

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-30 17:05:01 -07:00
Ashwin Bhat
ae800c5ec8
feat: add max_budget_usd option to Python SDK (#293)
Add support for limiting API costs using the max_budget_usd option,
mirroring the TypeScript SDK functionality. When the budget is exceeded,
query execution stops and returns a result with subtype
'error_max_budget_usd'.

- Add max_budget_usd field to ClaudeAgentOptions
- Pass --max-budget-usd flag to Claude Code CLI
- Add test coverage for budget limit behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-30 16:45:35 -07:00
blois
7be296f12e
feat: add max_thinking_tokens option to ClaudeAgentOptions (#298)
Add support for controlling the maximum number of tokens allocated to
extended thinking blocks via the max_thinking_tokens parameter.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-30 15:14:42 -07:00
Ashwin Bhat
fd4e33d4b9
feat: add /generate-changelog slash command (#287)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-examples (3.10) (push) Has been cancelled
Test / test-examples (3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.12) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Move changelog generation prompt from publish.yml into a reusable slash
command at .claude/commands/generate-changelog.md. Update workflow to
call the command with version parameters.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-24 16:21:55 -07:00
github-actions[bot]
68eb68b740
chore: release v0.1.5 (#286)
This PR updates the version to 0.1.5 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_agent_sdk/_version.py`
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.5/
- Install with: `pip install claude-agent-sdk==0.1.5`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-24 16:04:33 -07:00
Ashwin Bhat
c5957634ac
feat: add plugin support to Python SDK (#285)
Add SdkPluginConfig type and plugins field to ClaudeAgentOptions.
Plugins can be loaded using the local type with a path to the plugin
directory.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-24 15:29:01 -07:00
github-actions[bot]
5656aeadd6
chore: release v0.1.4 (#260)
Some checks are pending
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
This PR updates the version to 0.1.4 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_agent_sdk/_version.py`
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.4/
- Install with: `pip install claude-agent-sdk==0.1.4`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-10-23 16:10:13 -07:00
Ashwin Bhat
9b8576158d
fix: fetch all git history including tags in publish workflow (#284)
The changelog generation step was failing to get the previous release
tag because the checkout action was doing a shallow clone. Adding
fetch-depth: 0 ensures all tags are available for git describe.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-23 15:03:41 -07:00
KuaaMU
41e220cc2c
fix: handle Windows command line length limit for --agents option (#245)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
## Summary

Fixes #238 - Resolves "command line too long" error on Windows when
using multiple subagents with long prompts.

## Problem

On Windows, the command line length is limited to 8191 characters
(cmd.exe). When using multiple subagents with long prompts, the
`--agents` JSON argument can easily exceed this limit, causing the
error:
```
命令行太长。 (command line too long)
Fatal error in message reader: Command failed with exit code 1
```

## Solution

This PR implements automatic detection and handling of command line
length limits:

1. **Platform-specific limits**: 
   - Windows: 8000 characters (safe margin below 8191)
   - Other platforms: 100,000 characters

2. **Automatic fallback**: When the command line would exceed the limit:
   - Write agents JSON to a temporary file
   - Use Claude CLI's `@filepath` syntax to reference the file
   - Clean up temp files when transport is closed

3. **Zero breaking changes**: The fix is transparent to users - it
automatically activates only when needed

## Changes

- Add `platform` and `tempfile` imports
- Add `_CMD_LENGTH_LIMIT` constant with platform-specific values
- Track temporary files in `self._temp_files` list
- Modify `_build_command()` to detect long command lines and use temp
files
- Clean up temp files in `close()` method

## Testing

-  All existing tests pass (122 tests)
-  Linting and type checking pass
-  Minimal changes - only 47 lines added/modified
-  Solution transparently handles the Windows command line limit

## Test plan

- [x] Test on Windows with multiple subagents and long prompts
- [x] Verify temp files are created and cleaned up properly
- [x] Verify normal operation (short command lines) is unaffected
- [x] Test cross-platform compatibility (limit only applies on Windows)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-23 10:16:47 -07:00
Ashwin Bhat
923d3d4620
feat: add CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK env var (#257)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-examples (3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.10) (push) Has been cancelled
Test / test-examples (3.12) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Adds environment variable to allow skipping the Claude Code version
check. Users can set CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK to disable the
check.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-15 14:17:04 -07:00
Arash Dabiri
f27c4ab4c7
feat: Added base64 image handling to sdk_mcp_server tool calling (#175)
With this change claude_code_sdk tools can now also return base64
images, in addition to text, following the MCP standard.

For example:
```
import base64

@tool("make_response", "prompt", {"foo": str})
def make_response(args):

    fake_bytes = b"someimagebytes"
    encoded_image = base64.b64encode(fake_bytes).decode("utf-8")

    return {
        "content": [
            {"type": "text", "text": "Hello world!"},
            {
                "type": "image",
                 "mimeType": "image/jpeg",
                 "data": encoded_image,
            },
        ]
    }
```

The image will now be sent to Claude Code when calling the function,
allowing it to react to it.

---------

Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-10-15 07:22:14 -07:00
Ashwin Bhat
f896cd6f7f
feat: add pre-push hook for lint checks (#254)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Adds a committable pre-push hook that runs ruff checks before pushing,
matching the CI lint workflow. Developers run ./scripts/initial-setup.sh
to install the hook locally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-14 09:26:49 -07:00
Chase Naples
aebcf9d6a4
feat: add cli_path support to ClaudeAgentOptions (#235)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.11) (push) Has been cancelled
Test / test-examples (3.12) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
## Summary
Adds support for passing custom Claude Code CLI paths through
`ClaudeAgentOptions`, allowing organizations with non-standard
installation locations to specify the CLI path explicitly.

## Motivation
As noted in #214, organizations may install Claude Code CLI (or wrapped
versions) at custom locations and prefer to provide those paths instead
of relying on the SDK's default search logic. The transport layer
already supported `cli_path`, but it was never exposed through the
public API.

## Changes
1. **types.py**: Added `cli_path: str | Path | None = None` parameter to
`ClaudeAgentOptions` dataclass
2. **_internal/client.py**: Pass `cli_path` from
`configured_options.cli_path` to `SubprocessCLITransport`
3. **client.py**: Pass `cli_path` from `options.cli_path` to
`SubprocessCLITransport`

## Implementation Details
The `SubprocessCLITransport` constructor already accepted a `cli_path`
parameter (line 40 of subprocess_cli.py), but it was never passed from
the client layers. This PR completes the wiring by:
- Adding the option to the public `ClaudeAgentOptions` interface
- Extracting and passing it through both client implementations
(`InternalClient.process_query` and `ClaudeSDKClient.connect`)

## Usage Example
```python
from claude_agent_sdk import query, ClaudeAgentOptions

# Specify custom CLI path
options = ClaudeAgentOptions(
    cli_path="/custom/path/to/claude"
)

result = await query("Hello!", options=options)
```

## Testing
- No new tests added as this is a straightforward parameter pass-through
- Existing tests should continue to work (default behavior unchanged)
- CI will validate the changes don't break existing functionality

Fixes #214

---------

Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-10-12 23:19:53 -07:00
github-actions[bot]
20c1b89734
chore: release v0.1.3 (#242)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-examples (3.10) (push) Has been cancelled
Test / test-examples (3.11) (push) Has been cancelled
Test / test-examples (3.12) (push) Has been cancelled
This PR updates the version to 0.1.3 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_agent_sdk/_version.py`
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.3/
- Install with: `pip install claude-agent-sdk==0.1.3`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-10-10 17:23:14 -07:00
Ashwin Bhat
1f080748d4
Revert "chore: temporarily disable Windows CI in test-e2e workflow" (#241)
Reverts anthropics/claude-agent-sdk-python#234
2025-10-10 16:40:58 -07:00
Ashwin Bhat
d754e5cc1d
feat: add strongly-typed hook inputs with TypedDict (#240)
Add typed hook input structures (PreToolUseHookInput,
PostToolUseHookInput, etc.) to provide better IDE autocomplete and type
safety for hook callbacks. Also convert HookContext from dataclass to
TypedDict to match runtime behavior.

Changes:
- Add BaseHookInput, PreToolUseHookInput, PostToolUseHookInput,
UserPromptSubmitHookInput, StopHookInput, SubagentStopHookInput, and
PreCompactHookInput TypedDict classes
- Update HookCallback signature to use HookInput union type
- Convert HookContext from dataclass to TypedDict (fixes type mismatch)
- Export all new hook input types from __init__.py
- Update all examples and tests to use typed hook inputs

Benefits:
- Zero breaking changes (TypedDict is dict-compatible at runtime)
- Full type safety and IDE autocomplete for hook callbacks
- Matches TypeScript SDK structure exactly
- Self-documenting hook input fields

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-10 16:22:13 -07:00
Ashwin Bhat
48b62a05a3
fix: convert Python-safe field names (async_, continue_) to CLI format (#239)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Fixes critical bug where hook outputs using `async_` and `continue_`
(Python-safe names avoiding keyword conflicts) were not being converted
to `async` and `continue` as expected by the CLI. This caused hook
control fields like `{"decision": "block"}` or `{"continue_": False}` to
be silently ignored.

Changes:
- Add _convert_hook_output_for_cli() to handle field name conversion
- Apply conversion in hook callback handling
- Update type documentation to clarify field name usage
- Add comprehensive test coverage for field name conversion
- Update existing tests to verify conversion occurs correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-10 10:25:35 -07:00
github-actions[bot]
67e77e928a
chore: release v0.1.2 (#236)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
This PR updates the version to 0.1.2 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_agent_sdk/_version.py`
- Updated `CHANGELOG.md` with release notes

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.2/
- Install with: `pip install claude-agent-sdk==0.1.2`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-09 19:56:53 -07:00
Ashwin Bhat
e8d7e71a0a
Add missing hook output fields to match TypeScript SDK (#226)
Closes the gap between Python and TypeScript SDK hook output types by
adding:
- `reason` field for explaining decisions
- `continue_` field for controlling execution flow
- `suppressOutput` field for hiding stdout
- `stopReason` field for stop explanations
- `decision` now supports both "approve" and "block" (not just "block")
- `AsyncHookJSONOutput` type for deferred hook execution
- Proper typing for `hookSpecificOutput` with discriminated unions

Also adds comprehensive examples and tests:
- New examples in hooks.py demonstrating all new fields
- Unit tests in test_tool_callbacks.py for new output types
- E2E tests in e2e-tests/test_hooks.py with real API calls
- CI integration in .github/workflows/test.yml

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-09 18:13:23 -07:00
Ashwin Bhat
5bea2dc27d
chore: temporarily disable Windows CI in test-e2e workflow (#234)
Temporarily removes windows-latest from the test-e2e job matrix to
disable Windows end-to-end testing. Unit tests continue to run on
Windows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-09 17:01:14 -07:00
Victor Mota
dcd51c9ecb
fix #227: Fix PermissionResultAllow conversion to control_response (#232)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test (macos-latest, 3.10) (push) Waiting to run
Test / test (macos-latest, 3.11) (push) Waiting to run
Test / test (macos-latest, 3.12) (push) Waiting to run
Test / test (macos-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (windows-latest, 3.10) (push) Waiting to run
Test / test (windows-latest, 3.11) (push) Waiting to run
Test / test (windows-latest, 3.12) (push) Waiting to run
Test / test (windows-latest, 3.13) (push) Waiting to run
Test / test-e2e (macos-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (macos-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (ubuntu-latest, 3.13) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.10) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.11) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.12) (push) Blocked by required conditions
Test / test-e2e (windows-latest, 3.13) (push) Blocked by required conditions
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
https://github.com/anthropics/claude-agent-sdk-python/issues/227

Fixes zod issues returned by cli subprocess transport - example now runs
correctly to completion:


```
============================================================
Tool Permission Callback Example
============================================================

This example demonstrates how to:
1. Allow/deny tools based on type
2. Modify tool inputs for safety
3. Log tool usage
4. Prompt for unknown tools
============================================================

📝 Sending query to Claude...

📨 Receiving response...

💬 Claude: I'll help you with these tasks. Let me execute them in sequence.

💬 Claude: Now I'll create a simple Python hello world script:

🔧 Tool Permission Request: Write
   Input: {
  "file_path": "/Users/vimota/code/claude-agent-sdk-python/hello.py",
  "content": "#!/usr/bin/env python3\n\nprint(\"Hello, World!\")\n"
}
   ⚠️  Redirecting write from /Users/vimota/code/claude-agent-sdk-python/hello.py to ./safe_output/hello.py

💬 Claude: Now let's run the script:

🔧 Tool Permission Request: Bash
   Input: {
  "command": "python hello.py",
  "description": "Run hello.py script"
}
    Allowing bash command: python hello.py

💬 Claude: Let me check where the file was created:

💬 Claude: I see the file was created in the `safe_output` directory. Let me run it from there:

🔧 Tool Permission Request: Bash
   Input: {
  "command": "python ./safe_output/hello.py",
  "description": "Run hello.py from safe_output"
}
    Allowing bash command: python ./safe_output/hello.py

💬 Claude: Perfect! All tasks completed successfully:

1. **Listed files** - The directory contains a Python SDK project with source code in `src/`, tests, examples, and configuration files.

2. **Created hello.py** - A simple Python script was created at `./safe_output/hello.py` with a basic "Hello, World!" print statement.

3. **Ran the script** - The script executed successfully and printed "Hello, World!" to the console.

Note: The file was created in the `safe_output/` subdirectory rather than the root directory.

 Task completed!
   Duration: 31158ms
   Cost: $0.0736
   Messages processed: 18

============================================================
Tool Usage Summary
============================================================

1. Tool: Write
   Input: {
      "file_path": "/Users/vimota/code/claude-agent-sdk-python/hello.py",
      "content": "#!/usr/bin/env python3\n\nprint(\"Hello, World!\")\n"
}
   Suggestions: [{'type': 'setMode', 'mode': 'acceptEdits', 'destination': 'session'}]

2. Tool: Bash
   Input: {
      "command": "python hello.py",
      "description": "Run hello.py script"
}
   Suggestions: [{'type': 'addRules', 'rules': [{'toolName': 'Bash', 'ruleContent': 'python:*'}], 'behavior': 'allow', 'destination': 'localSettings'}]

3. Tool: Bash
   Input: {
      "command": "python ./safe_output/hello.py",
      "description": "Run hello.py from safe_output"
}
   Suggestions: [{'type': 'addRules', 'rules': [{'toolName': 'Bash', 'ruleContent': 'python:*'}], 'behavior': 'allow', 'destination': 'localSettings'}]
```
2025-10-09 12:24:13 -07:00
Ashwin Bhat
71a85ac9aa
feat: automate changelog updates in release workflow (#231)
Replace GitHub API-based commits with local git workflow and integrate
claude-code-action to automatically generate changelog entries. The
workflow now:
- Creates release branch locally with version commits
- Uses Claude to review changes and update CHANGELOG.md
- Pushes complete branch with all commits together

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-09 10:36:54 -07:00
Ashwin Bhat
c2b72f1cc5
Add CHANGELOG entry for version 0.1.1 (#229)
Document the features and improvements released in version 0.1.1,
including minimum Claude Code version check, updated PermissionResult
types, and simplified model references.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-09 08:27:58 -07:00
Ashwin Bhat
6793e40264
Add Windows support to test workflows (#222)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.10) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.11) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test-e2e (macos-latest, 3.13) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test-e2e (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.10) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.11) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.12) (push) Has been cancelled
Test / test-e2e (windows-latest, 3.13) (push) Has been cancelled
Test / test-examples (3.10) (push) Has been cancelled
Test / test-examples (3.11) (push) Has been cancelled
Test / test-examples (3.12) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Add cross-platform testing for Windows alongside Linux across all test
jobs (unit tests, e2e tests, and examples). Uses native Windows
installation via PowerShell script and platform-specific timeout
handling for example scripts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-07 17:28:24 -07:00
KuaaMU
14f0714a8c
Fix Windows test failures due to path separator issues (#219)
- Make test_cli_path_accepts_pathlib_path platform-aware by comparing
with str(path)
- Fix test_build_command_with_add_dirs to check directories individually
- Fix test_build_command_with_mcp_servers_as_file_path to handle Path
conversion
- Fix test_query_with_async_iterable to properly execute Python scripts
on Windows

All tests now pass on both Windows and Unix-like systems (110/110 tests
passing).

Fixes the issue #217 where pathlib.Path automatically converts path
separators based on the operating system, causing test assertions to
fail on Windows.
2025-10-07 16:18:30 -07:00
github-actions[bot]
333491eef5
chore: bump version to 0.1.1 (#210)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (3.10) (push) Waiting to run
Test / test (3.11) (push) Waiting to run
Test / test (3.12) (push) Waiting to run
Test / test (3.13) (push) Waiting to run
Test / test-e2e (3.12) (push) Blocked by required conditions
Test / test-e2e (3.13) (push) Blocked by required conditions
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-e2e (3.10) (push) Blocked by required conditions
Test / test-e2e (3.11) (push) Blocked by required conditions
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
This PR updates the version to 0.1.1 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_agent_sdk/_version.py`

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.1/
- Install with: `pip install claude-agent-sdk==0.1.1`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-06 18:22:30 -07:00
Ashwin Bhat
24408f9ddd
update PermissionResult to match latest control protocol (#209)
Recreation of
https://github.com/anthropics/claude-agent-sdk-python/pull/174, with
signed commits
2025-10-06 14:01:16 -07:00
Ashwin Bhat
70358589cf
Add minimum Claude Code version check (2.0.0+) (#206)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (3.10) (push) Has been cancelled
Test / test (3.11) (push) Has been cancelled
Test / test (3.12) (push) Has been cancelled
Test / test (3.13) (push) Has been cancelled
Test / test-e2e (3.10) (push) Has been cancelled
Test / test-e2e (3.11) (push) Has been cancelled
Test / test-e2e (3.12) (push) Has been cancelled
Test / test-e2e (3.13) (push) Has been cancelled
Test / test-examples (3.10) (push) Has been cancelled
Test / test-examples (3.11) (push) Has been cancelled
Test / test-examples (3.12) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
- 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 <noreply@anthropic.com>
2025-10-04 21:24:38 -07:00
Ashwin Bhat
2a9693e258
Update model references to claude-sonnet-4-5 (#198)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (3.10) (push) Has been cancelled
Test / test (3.11) (push) Has been cancelled
Test / test (3.12) (push) Has been cancelled
Test / test (3.13) (push) Has been cancelled
Test / test-e2e (3.10) (push) Has been cancelled
Test / test-e2e (3.11) (push) Has been cancelled
Test / test-e2e (3.12) (push) Has been cancelled
Test / test-e2e (3.13) (push) Has been cancelled
Test / test-examples (3.10) (push) Has been cancelled
Test / test-examples (3.11) (push) Has been cancelled
Test / test-examples (3.12) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Replace all dated model references (claude-sonnet-4-20250514,
claude-3-5-sonnet-20241022) with the simplified claude-sonnet-4-5
identifier across examples, tests, and documentation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-30 12:59:14 -07:00
Ashwin Bhat
af870623e7
changelog update (#192)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (3.10) (push) Waiting to run
Test / test (3.11) (push) Waiting to run
Test / test (3.12) (push) Waiting to run
Test / test (3.13) (push) Waiting to run
Test / test-e2e (3.10) (push) Blocked by required conditions
Test / test-e2e (3.11) (push) Blocked by required conditions
Test / test-e2e (3.12) (push) Blocked by required conditions
Test / test-e2e (3.13) (push) Blocked by required conditions
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-29 08:37:55 -07:00
wenxiang
d9dd841912
feat: ClaudeSDKClient supports custom transport. (#187)
`query` supports custom transport, but ClaudeSDKClient does not. So I
add this feature. eg.

```python
from claude_code_sdk import ClaudeSDKClient, Transport

class MyCustomTransport(Transport):
    # Implement custom transport logic
    pass

transport = MyCustomTransport()
async with ClaudeSDKClient( transport=transport) as client:
    await client.query("Greet Alice")

    # Extract and print response
    async for msg in client.receive_response():
        print(msg)
```
2025-09-29 08:24:49 -07:00
github-actions[bot]
9d4659c97f
chore: bump version to 0.1.0 (#191)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (3.10) (push) Waiting to run
Test / test (3.11) (push) Waiting to run
Test / test (3.12) (push) Waiting to run
Test / test (3.13) (push) Waiting to run
Test / test-e2e (3.10) (push) Blocked by required conditions
Test / test-e2e (3.11) (push) Blocked by required conditions
Test / test-e2e (3.12) (push) Blocked by required conditions
Test / test-e2e (3.13) (push) Blocked by required conditions
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
This PR updates the version to 0.1.0 after publishing to PyPI.

## Changes
- Updated version in `pyproject.toml`
- Updated version in `src/claude_agent_sdk/_version.py`

## Release Information
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/0.1.0/
- Install with: `pip install claude-agent-sdk==0.1.0`

🤖 Generated by GitHub Actions

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-28 16:48:55 -07:00
Michael Gendy
4b9cfc76e4
Make CLI buffer limit configurable (#190) 2025-09-28 15:45:06 -07:00
Ashwin Bhat
d86c47f2d6
refactor: remove unnecessary node installation check (#189)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (3.10) (push) Waiting to run
Test / test (3.11) (push) Waiting to run
Test / test (3.12) (push) Waiting to run
Test / test (3.13) (push) Waiting to run
Test / test-e2e (3.10) (push) Blocked by required conditions
Test / test-e2e (3.11) (push) Blocked by required conditions
Test / test-e2e (3.12) (push) Blocked by required conditions
Test / test-e2e (3.13) (push) Blocked by required conditions
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Simplify CLI detection by removing redundant node installation check
before throwing CLINotFoundError.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-28 15:16:40 -07:00
Dickson Tsai
b3656b1765
Rename claude_code to claude_agent (#188) 2025-09-28 14:52:53 -07:00
Dickson Tsai
180d64887a
feat: add stderr callback to capture CLI debug output (#170)
## Summary
- Add stderr callback option to ClaudeCodeOptions to capture CLI
subprocess stderr output
- Matches TypeScript SDK's stderr callback behavior for feature parity
- Useful for debugging and monitoring CLI operations

## Changes
- Added `stderr: Callable[[str], None] | None` field to
`ClaudeCodeOptions`
- Updated `SubprocessCLITransport` to handle stderr streaming with async
task
- Added example demonstrating stderr callback usage
- Added e2e tests to verify functionality

## Test plan
- [x] Run e2e tests: `python -m pytest e2e-tests/test_stderr_callback.py
-v`
- [x] Run example: `python examples/stderr_callback_example.py`
- [x] Verify backward compatibility with existing `debug_stderr` field
- [x] All linting and type checks pass

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-28 14:10:10 -07:00
Dickson Tsai
62289d2dce
feat: add dynamic permission mode and model switching to ClaudeSDKClient (#171)
## Summary
- Adds `set_permission_mode()` method to dynamically change permission
modes during streaming sessions
- Adds `set_model()` method to switch AI models mid-conversation
- Implements control protocol support matching the TypeScript SDK's
capabilities

## Motivation
The TypeScript SDK supports dynamic control through
`setPermissionMode()` and `setModel()` methods on the Query interface.
This PR brings the same functionality to the Python SDK, allowing users
to:

1. Start with restrictive permissions for code review, then switch to
`acceptEdits` for implementation
2. Use different models for different parts of a task (e.g., Sonnet for
complex analysis, Haiku for simple tasks)
3. Adjust permissions based on workflow needs without restarting
sessions

## Changes
- **ClaudeSDKClient**: Added `set_permission_mode(mode)` and
`set_model(model)` methods
- **Internal Query class**: Added `set_model(model)` method to send
control requests
- **E2E tests**: Added comprehensive tests verifying the functionality
works with real API calls

## Test Plan
- [x] All existing unit tests pass (102 tests)
- [x] New E2E tests added and passing:
- `test_set_permission_mode`: Verifies permission mode changes take
effect
  - `test_set_model`: Confirms model switching works mid-conversation
  - `test_interrupt`: Validates interrupt capability
- [x] Type checking passes (`mypy`)
- [x] Linting passes (`ruff`)

## Usage Example
```python
async with ClaudeSDKClient() as client:
    # Start with default permissions for review
    await client.query("Analyze this code for issues")
    
    # Switch to auto-accept edits for implementation
    await client.set_permission_mode('acceptEdits')
    await client.query("Now fix the issues we found")
    
    # Use a different model for simpler tasks
    await client.set_model('claude-3-5-haiku-20241022')
    await client.query("Add a simple docstring")
```

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-09-28 14:09:29 -07:00
Dickson Tsai
0d2404e5d9
Rename ClaudeCodeOptions to ClaudeAgentOptions (#185)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (3.10) (push) Has been cancelled
Test / test (3.11) (push) Has been cancelled
Test / test (3.12) (push) Has been cancelled
Test / test (3.13) (push) Has been cancelled
Test / test-e2e (3.10) (push) Has been cancelled
Test / test-e2e (3.11) (push) Has been cancelled
Test / test-e2e (3.12) (push) Has been cancelled
Test / test-e2e (3.13) (push) Has been cancelled
Test / test-examples (3.10) (push) Has been cancelled
Test / test-examples (3.11) (push) Has been cancelled
Test / test-examples (3.12) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
2025-09-26 21:38:05 -07:00
Ashwin Bhat
dbb153b1f6
feat: refactor system_prompt to support preset and append options (#183)
Some checks are pending
Lint / lint (push) Waiting to run
Test / test (3.10) (push) Waiting to run
Test / test (3.11) (push) Waiting to run
Test / test (3.12) (push) Waiting to run
Test / test (3.13) (push) Waiting to run
Test / test-e2e (3.10) (push) Blocked by required conditions
Test / test-e2e (3.11) (push) Blocked by required conditions
Test / test-e2e (3.12) (push) Blocked by required conditions
Test / test-e2e (3.13) (push) Blocked by required conditions
Test / test-examples (3.10) (push) Blocked by required conditions
Test / test-examples (3.11) (push) Blocked by required conditions
Test / test-examples (3.12) (push) Blocked by required conditions
Test / test-examples (3.13) (push) Blocked by required conditions
Replace separate system_prompt and append_system_prompt fields with a
single system_prompt field that accepts:
- string: custom system prompt
- {"preset": "claude_code"}: use default Claude Code prompt
- {"preset": "claude_code", "append": "..."}: default prompt with
additions
- None/undefined: vanilla Claude with no system prompt

This matches the TypeScript SDK API design and provides more flexible
system prompt configuration.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-26 12:59:27 -07:00
Ashwin Bhat
507e22cb4a
feat: add agents and setting sources support (#182)
## Summary
- Add support for custom agent definitions via `agents` option
- Add support for controlling setting sources via `setting_sources`
option
- Add `/commit` slash command to project
- Add examples demonstrating both features
- Add e2e tests for verification

## Changes

### Core Implementation
- Add `AgentDefinition` and `SettingSource` types to `types.py`
- Add `agents` and `setting_sources` fields to `ClaudeCodeOptions`
- Update subprocess CLI transport to pass `--agents` and
`--setting-sources` flags
- **Default behavior**: When `setting_sources` is not provided, pass
empty string (no settings loaded)
- Handle empty `setting_sources` array correctly (pass empty string to
CLI)

### Examples
- `examples/agents.py`: Demonstrates custom agent definitions with
different tools and models
- `examples/setting_sources.py`: Shows how setting sources control which
settings are loaded
  - Default behavior (no settings)
  - User-only settings
  - User + project settings

### Tests
- Add e2e tests verifying agents and setting_sources functionality
- Test default behavior (no settings loaded)
- Test filtering by setting source
- Use `output_style` checking to verify settings loaded/not loaded
- Tests use temporary directories for isolated testing

### Project Config
- Add `.claude/commands/commit.md` slash command for git commits

## Test Plan
- [x] E2E tests added for all new functionality
- [ ] CI tests pass
- [ ] Examples run successfully

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-26 12:52:46 -07:00
Ashwin Bhat
233cefa3e1
feat: add version environment variable (#184)
Include the SDK version in the environment when spawning the Claude CLI
process.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-26 12:23:33 -07:00
blois
cfdd28a254
feat: add fork_session option for resuming sessions (#179)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (3.10) (push) Has been cancelled
Test / test (3.11) (push) Has been cancelled
Test / test (3.12) (push) Has been cancelled
Test / test (3.13) (push) Has been cancelled
Test / test-e2e (3.11) (push) Has been cancelled
Test / test-e2e (3.13) (push) Has been cancelled
Test / test-examples (3.10) (push) Has been cancelled
Test / test-examples (3.11) (push) Has been cancelled
Test / test-examples (3.12) (push) Has been cancelled
Test / test-e2e (3.12) (push) Has been cancelled
Test / test-e2e (3.10) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Add support for fork_session parameter which allows resumed sessions to
fork to a new session ID rather than continuing the previous session.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-23 21:34:22 -07:00
Ashwin Bhat
12fdca9b1b
feat: improve GitHub release notes generation (#173)
Some checks failed
Lint / lint (push) Has been cancelled
Test / test (3.10) (push) Has been cancelled
Test / test (3.11) (push) Has been cancelled
Test / test (3.12) (push) Has been cancelled
Test / test (3.13) (push) Has been cancelled
Test / test-e2e (3.10) (push) Has been cancelled
Test / test-e2e (3.11) (push) Has been cancelled
Test / test-e2e (3.12) (push) Has been cancelled
Test / test-e2e (3.13) (push) Has been cancelled
Test / test-examples (3.10) (push) Has been cancelled
Test / test-examples (3.11) (push) Has been cancelled
Test / test-examples (3.12) (push) Has been cancelled
Test / test-examples (3.13) (push) Has been cancelled
Use `gh release create --generate-notes` to automatically include PR
titles, commits, and contributors in release notes instead of static
template text.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-17 20:09:35 -07:00
74 changed files with 6005 additions and 1222 deletions

View file

@ -0,0 +1,9 @@
---
name: test-agent
description: A simple test agent for SDK testing
tools: Read
---
# Test Agent
You are a simple test agent. When asked a question, provide a brief, helpful answer.

View file

@ -0,0 +1,17 @@
---
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*)
description: Create a git commit
---
## Context
- Current git status: !`git status`
- Current git diff (staged and unstaged changes): !`git diff HEAD`
- Current branch: !`git branch --show-current`
- Recent commits: !`git log --oneline -10`
## Your task
Based on the above changes, create a single git commit.
You have the capability to call multiple tools in a single response. Stage and create the commit using a single message. Do not use any other tools or do anything else. Do not send any other text or messages besides these tool calls.

View file

@ -0,0 +1,19 @@
---
allowed-tools: Edit, Bash(git add:*), Bash(git commit:*)
description: Generate changelog for a new release version
---
You are updating the changelog for the new release.
Update CHANGELOG.md to add a new section for the new version at the top of the file, right after the '# Changelog' heading.
Review the recent commits and merged pull requests since the last release to generate meaningful changelog content for the new version. Follow the existing format in CHANGELOG.md with sections like:
- Breaking Changes (if any)
- New Features
- Bug Fixes
- Documentation
- Internal/Other changes
Include only the sections that are relevant based on the actual changes. Write clear, user-focused descriptions.
After updating CHANGELOG.md, commit the changes with the message "docs: update changelog for v{new_version}".

49
.dockerignore Normal file
View file

@ -0,0 +1,49 @@
# Git
.git
.gitignore
# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.env
.venv
env/
venv/
ENV/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Testing/Coverage
.coverage
.pytest_cache/
htmlcov/
.tox/
.nox/
# Misc
*.log
.DS_Store

View file

@ -24,12 +24,6 @@ jobs:
VERSION="${BRANCH_NAME#release/v}" VERSION="${BRANCH_NAME#release/v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Get previous release tag
id: previous_tag
run: |
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "")
echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
- name: Create and push tag - name: Create and push tag
run: | run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.email "github-actions[bot]@users.noreply.github.com"
@ -43,23 +37,37 @@ jobs:
git push origin "v${{ steps.extract_version.outputs.version }}" git push origin "v${{ steps.extract_version.outputs.version }}"
- name: Create GitHub Release - name: Create GitHub Release
uses: actions/create-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: run: |
tag_name: v${{ steps.extract_version.outputs.version }} VERSION="${{ steps.extract_version.outputs.version }}"
release_name: Release v${{ steps.extract_version.outputs.version }}
body: | # Extract changelog section for this version to a temp file
## Release v${{ steps.extract_version.outputs.version }} awk -v ver="$VERSION" '
/^## / {
Published to PyPI: https://pypi.org/project/claude-code-sdk/${{ steps.extract_version.outputs.version }}/ if (found) exit
if ($2 == ver) found=1
### Installation next
```bash }
pip install claude-code-sdk==${{ steps.extract_version.outputs.version }} found { print }
``` ' CHANGELOG.md > release_notes.md
### What's Changed # Append install instructions
See the [full changelog](https://github.com/${{ github.repository }}/compare/${{ steps.previous_tag.outputs.previous_tag }}...v${{ steps.extract_version.outputs.version }}) {
draft: false echo ""
prerelease: false echo "---"
echo ""
echo "**PyPI:** https://pypi.org/project/claude-agent-sdk/VERSION/"
echo ""
echo '```bash'
echo "pip install claude-agent-sdk==VERSION"
echo '```'
} >> release_notes.md
# Replace VERSION placeholder
sed -i "s/VERSION/$VERSION/g" release_notes.md
# Create release with notes from file
gh release create "v$VERSION" \
--title "v$VERSION" \
--notes-file release_notes.md

View file

@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version: version:
description: 'Version to publish (e.g., 0.1.0)' description: 'Package version to publish (e.g., 0.1.4)'
required: true required: true
type: string type: string
jobs: jobs:
@ -12,89 +12,141 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ['3.10', '3.11', '3.12', '3.13'] python-version: ["3.10", "3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -e ".[dev]" pip install -e ".[dev]"
- name: Run tests - name: Run tests
run: | run: |
python -m pytest tests/ -v python -m pytest tests/ -v
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Run ruff
run: |
ruff check src/ tests/
ruff format --check src/ tests/
- name: Run mypy
run: |
mypy src/
build-wheels:
needs: [test, lint]
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
permissions:
contents: write
pull-requests: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.12' python-version: '3.12'
- name: Install dependencies - name: Install build dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -e ".[dev]" pip install build twine wheel
shell: bash
- name: Run ruff
- name: Build wheel with bundled CLI
run: | run: |
ruff check src/ tests/ python scripts/build_wheel.py \
ruff format --check src/ tests/ --version "${{ github.event.inputs.version }}" \
--skip-sdist \
- name: Run mypy --clean
run: | shell: bash
mypy src/
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: wheel-${{ matrix.os }}
path: dist/*.whl
if-no-files-found: error
compression-level: 0
publish: publish:
needs: [test, lint] needs: [build-wheels]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0 # Fetch all history including tags for changelog generation
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.12' python-version: '3.12'
- name: Set version - name: Set version
id: version id: version
run: | run: |
VERSION="${{ github.event.inputs.version }}" VERSION="${{ github.event.inputs.version }}"
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update version - name: Update version
run: | run: |
python scripts/update_version.py "${{ env.VERSION }}" python scripts/update_version.py "${{ env.VERSION }}"
- name: Read CLI version from code
id: cli_version
run: |
CLI_VERSION=$(python -c "import re; print(re.search(r'__cli_version__ = \"([^\"]+)\"', open('src/claude_agent_sdk/_cli_version.py').read()).group(1))")
echo "cli_version=$CLI_VERSION" >> $GITHUB_OUTPUT
echo "Bundled CLI version: $CLI_VERSION"
- name: Download all wheel artifacts
uses: actions/download-artifact@v4
with:
path: dist
pattern: wheel-*
merge-multiple: true
- name: Install build dependencies - name: Install build dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install build twine pip install build twine
- name: Build package - name: Build source distribution
run: python -m build run: python -m build --sdist
- name: Check package
run: twine check dist/*
- name: Publish to PyPI - name: Publish to PyPI
env: env:
TWINE_USERNAME: __token__ TWINE_USERNAME: __token__
@ -102,68 +154,69 @@ jobs:
run: | run: |
twine upload dist/* twine upload dist/*
echo "Package published to PyPI" echo "Package published to PyPI"
echo "Install with: pip install claude-code-sdk==${{ env.VERSION }}" echo "Install with: pip install claude-agent-sdk==${{ env.VERSION }}"
- name: Create version update PR - name: Get previous release tag
env: id: previous_tag
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: |
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
echo "Previous release: $PREVIOUS_TAG"
- name: Create release branch and commit version changes
run: | run: |
# Create a new branch for the version update # Create a new branch for the version update
BRANCH_NAME="release/v${{ env.VERSION }}" BRANCH_NAME="release/v${{ env.VERSION }}"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
# Create branch via API # Configure git
BASE_SHA=$(git rev-parse HEAD) git config --local user.email "github-actions[bot]@users.noreply.github.com"
gh api \ git config --local user.name "github-actions[bot]"
--method POST \
/repos/$GITHUB_REPOSITORY/git/refs \ # Create and switch to new branch
-f ref="refs/heads/$BRANCH_NAME" \ git checkout -b "$BRANCH_NAME"
-f sha="$BASE_SHA"
# Commit version changes
# Get current SHA values of files git add pyproject.toml src/claude_agent_sdk/_version.py
echo "Getting SHA for pyproject.toml" git commit -m "chore: release v${{ env.VERSION }}"
PYPROJECT_SHA=$(gh api /repos/$GITHUB_REPOSITORY/contents/pyproject.toml --jq '.sha')
echo "Getting SHA for __init__.py" - name: Update changelog with Claude
INIT_SHA=$(gh api /repos/$GITHUB_REPOSITORY/contents/src/claude_code_sdk/__init__.py --jq '.sha') continue-on-error: true
uses: anthropics/claude-code-action@v1
# Commit pyproject.toml via GitHub API (this creates signed commits) with:
message="chore: bump version to ${{ env.VERSION }}" prompt: "/generate-changelog new version: ${{ env.VERSION }}, old version: ${{ steps.previous_tag.outputs.previous_tag }}"
base64 -i pyproject.toml > pyproject.toml.b64 anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
gh api \ github_token: ${{ secrets.GITHUB_TOKEN }}
--method PUT \ claude_args: |
/repos/$GITHUB_REPOSITORY/contents/pyproject.toml \ --model claude-opus-4-5
-f message="$message" \ --allowedTools 'Bash(git add:*),Bash(git commit:*),Edit'
-F content=@pyproject.toml.b64 \
-f sha="$PYPROJECT_SHA" \ - name: Push branch and create PR
-f branch="$BRANCH_NAME" env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Commit __init__.py via GitHub API run: |
base64 -i src/claude_code_sdk/__init__.py > init.py.b64 # Push the branch with all commits
gh api \ git push origin "${{ env.BRANCH_NAME }}"
--method PUT \
/repos/$GITHUB_REPOSITORY/contents/src/claude_code_sdk/__init__.py \
-f message="$message" \
-F content=@init.py.b64 \
-f sha="$INIT_SHA" \
-f branch="$BRANCH_NAME"
# Create PR using GitHub CLI # Create PR using GitHub CLI
PR_BODY="This PR updates the version to ${{ env.VERSION }} after publishing to PyPI. PR_BODY="This PR updates the version to ${{ env.VERSION }} after publishing to PyPI.
## Changes ## Changes
- Updated version in \`pyproject.toml\` - Updated version in \`pyproject.toml\` to ${{ env.VERSION }}
- Updated version in \`src/claude_code_sdk/__init__.py\` - Updated version in \`src/claude_agent_sdk/_version.py\` to ${{ env.VERSION }}
- Updated \`CHANGELOG.md\` with release notes
## Release Information ## Release Information
- Published to PyPI: https://pypi.org/project/claude-code-sdk/${{ env.VERSION }}/ - Published to PyPI: https://pypi.org/project/claude-agent-sdk/${{ env.VERSION }}/
- Install with: \`pip install claude-code-sdk==${{ env.VERSION }}\` - Bundled CLI version: ${{ steps.cli_version.outputs.cli_version }}
- Install with: \`pip install claude-agent-sdk==${{ env.VERSION }}\`
🤖 Generated by GitHub Actions" 🤖 Generated by GitHub Actions"
PR_URL=$(gh pr create \ PR_URL=$(gh pr create \
--title "chore: bump version to ${{ env.VERSION }}" \ --title "chore: release v${{ env.VERSION }}" \
--body "$PR_BODY" \ --body "$PR_BODY" \
--base main \ --base main \
--head "$BRANCH_NAME") --head "${{ env.BRANCH_NAME }}")
echo "PR created: $PR_URL" echo "PR created: $PR_URL"

View file

@ -0,0 +1,36 @@
name: Post new issues to Slack
on:
issues:
types: [opened]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Post to Slack
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # 2.1.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
{
"channel": "C09HY5E0K60",
"text": "New issue opened in ${{ github.repository }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*New Issue:* <${{ github.event.issue.html_url }}|#${{ github.event.issue.number }} ${{ github.event.issue.title }}>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Author:* ${{ github.event.issue.user.login }}"
}
}
]
}

View file

@ -8,9 +8,10 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"] python-version: ["3.10", "3.11", "3.12", "3.13"]
steps: steps:
@ -28,7 +29,7 @@ jobs:
- name: Run tests - name: Run tests
run: | run: |
python -m pytest tests/ -v --cov=claude_code_sdk --cov-report=xml python -m pytest tests/ -v --cov=claude_agent_sdk --cov-report=xml
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
@ -37,10 +38,11 @@ jobs:
fail_ci_if_error: false fail_ci_if_error: false
test-e2e: test-e2e:
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
needs: test # Run after unit tests pass needs: test # Run after unit tests pass
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"] python-version: ["3.10", "3.11", "3.12", "3.13"]
steps: steps:
@ -51,11 +53,20 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install Claude Code - name: Install Claude Code (Linux/macOS)
if: runner.os == 'Linux' || runner.os == 'macOS'
run: | run: |
curl -fsSL https://claude.ai/install.sh | bash curl -fsSL https://claude.ai/install.sh | bash
echo "$HOME/.local/bin" >> $GITHUB_PATH echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install Claude Code (Windows)
if: runner.os == 'Windows'
run: |
irm https://claude.ai/install.ps1 | iex
$claudePath = "$env:USERPROFILE\.local\bin"
echo "$claudePath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
shell: pwsh
- name: Verify Claude Code installation - name: Verify Claude Code installation
run: claude -v run: claude -v
@ -70,12 +81,30 @@ jobs:
run: | run: |
python -m pytest e2e-tests/ -v -m e2e python -m pytest e2e-tests/ -v -m e2e
test-e2e-docker:
runs-on: ubuntu-latest
needs: test # Run after unit tests pass
# Run e2e tests in Docker to catch container-specific issues like #406
steps:
- uses: actions/checkout@v4
- name: Build Docker test image
run: docker build -f Dockerfile.test -t claude-sdk-test .
- name: Run e2e tests in Docker
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
docker run --rm -e ANTHROPIC_API_KEY \
claude-sdk-test python -m pytest e2e-tests/ -v -m e2e
test-examples: test-examples:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: test-e2e # Run after e2e tests needs: test-e2e # Run after e2e tests
strategy: strategy:
matrix: matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"] python-version: ["3.13"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -85,11 +114,20 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install Claude Code - name: Install Claude Code (Linux)
if: runner.os == 'Linux'
run: | run: |
curl -fsSL https://claude.ai/install.sh | bash curl -fsSL https://claude.ai/install.sh | bash
echo "$HOME/.local/bin" >> $GITHUB_PATH echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install Claude Code (Windows)
if: runner.os == 'Windows'
run: |
irm https://claude.ai/install.ps1 | iex
$claudePath = "$env:USERPROFILE\.local\bin"
echo "$claudePath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
shell: pwsh
- name: Verify Claude Code installation - name: Verify Claude Code installation
run: claude -v run: claude -v
@ -98,9 +136,34 @@ jobs:
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -e . pip install -e .
- name: Run example scripts - name: Run example scripts (Linux)
if: runner.os == 'Linux'
env: env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: | run: |
python examples/quick_start.py python examples/quick_start.py
timeout 120 python examples/streaming_mode.py all timeout 120 python examples/streaming_mode.py all
timeout 120 python examples/hooks.py PreToolUse
timeout 120 python examples/hooks.py DecisionFields
- name: Run example scripts (Windows)
if: runner.os == 'Windows'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
python examples/quick_start.py
$job = Start-Job { python examples/streaming_mode.py all }
Wait-Job $job -Timeout 120 | Out-Null
Stop-Job $job
Receive-Job $job
$job = Start-Job { python examples/hooks.py PreToolUse }
Wait-Job $job -Timeout 120 | Out-Null
Stop-Job $job
Receive-Job $job
$job = Start-Job { python examples/hooks.py DecisionFields }
Wait-Job $job -Timeout 120 | Out-Null
Stop-Job $job
Receive-Job $job
shell: pwsh

1
.gitignore vendored
View file

@ -26,6 +26,7 @@ venv/
ENV/ ENV/
env/ env/
.venv .venv
uv.lock
# IDEs # IDEs
.vscode/ .vscode/

View file

@ -1,5 +1,204 @@
# Changelog # Changelog
## 0.1.18
### Internal/Other Changes
- **Docker-based test infrastructure**: Added Docker support for running e2e tests in containerized environments, helping catch Docker-specific issues (#424)
- Updated bundled Claude CLI to version 2.0.72
## 0.1.17
### New Features
- **UserMessage UUID field**: Added `uuid` field to `UserMessage` response type, making it easier to use the `rewind_files()` method by providing direct access to message identifiers needed for file checkpointing (#418)
### Internal/Other Changes
- Updated bundled Claude CLI to version 2.0.70
## 0.1.16
### Bug Fixes
- **Rate limit detection**: Fixed parsing of the `error` field in `AssistantMessage`, enabling applications to detect and handle API errors like rate limits. Previously, the `error` field was defined but never populated from CLI responses (#405)
### Internal/Other Changes
- Updated bundled Claude CLI to version 2.0.68
## 0.1.15
### New Features
- **File checkpointing and rewind**: Added `enable_file_checkpointing` option to `ClaudeAgentOptions` and `rewind_files(user_message_id)` method to `ClaudeSDKClient` and `Query`. This enables reverting file changes made during a session back to a specific checkpoint, useful for exploring different approaches or recovering from unwanted modifications (#395)
### Documentation
- Added license and terms section to README (#399)
## 0.1.14
### Internal/Other Changes
- Updated bundled Claude CLI to version 2.0.62
## 0.1.13
### Bug Fixes
- **Faster error handling**: CLI errors (e.g., invalid session ID) now propagate to pending requests immediately instead of waiting for the 60-second timeout (#388)
- **Pydantic 2.12+ compatibility**: Fixed `PydanticUserError` caused by `McpServer` type only being imported under `TYPE_CHECKING` (#385)
- **Concurrent subagent writes**: Added write lock to prevent `BusyResourceError` when multiple subagents invoke MCP tools in parallel (#391)
### Internal/Other Changes
- Updated bundled Claude CLI to version 2.0.59
## 0.1.12
### New Features
- **Tools option**: Added `tools` option to `ClaudeAgentOptions` for controlling the base set of available tools, matching the TypeScript SDK functionality. Supports three modes:
- Array of tool names to specify which tools should be available (e.g., `["Read", "Edit", "Bash"]`)
- Empty array `[]` to disable all built-in tools
- Preset object `{"type": "preset", "preset": "claude_code"}` to use the default Claude Code toolset
- **SDK beta support**: Added `betas` option to `ClaudeAgentOptions` for enabling Anthropic API beta features. Currently supports `"context-1m-2025-08-07"` for extended context window
## 0.1.11
### Internal/Other Changes
- Updated bundled Claude CLI to version 2.0.57
## 0.1.10
### Internal/Other Changes
- Updated bundled Claude CLI to version 2.0.53
## 0.1.9
### Internal/Other Changes
- Updated bundled Claude CLI to version 2.0.49
## 0.1.8
### Features
- Claude Code is now included by default in the package, removing the requirement to install it separately. If you do wish to use a separately installed build, use the `cli_path` field in `Options`.
## 0.1.7
### Features
- **Structured outputs support**: Agents can now return validated JSON matching your schema. See https://docs.claude.com/en/docs/agent-sdk/structured-outputs. (#340)
- **Fallback model handling**: Added automatic fallback model handling for improved reliability and parity with the TypeScript SDK. When the primary model is unavailable, the SDK will automatically use a fallback model (#317)
- **Local Claude CLI support**: Added support for using a locally installed Claude CLI from `~/.claude/local/claude`, enabling development and testing with custom Claude CLI builds (#302)
## 0.1.6
### Features
- **Max budget control**: Added `max_budget_usd` option to set a maximum spending limit in USD for SDK sessions. When the budget is exceeded, the session will automatically terminate, helping prevent unexpected costs (#293)
- **Extended thinking configuration**: Added `max_thinking_tokens` option to control the maximum number of tokens allocated for Claude's internal reasoning process. This allows fine-tuning of the balance between response quality and token usage (#298)
### Bug Fixes
- **System prompt defaults**: Fixed issue where a default system prompt was being used when none was specified. The SDK now correctly uses an empty system prompt by default, giving users full control over agent behavior (#290)
## 0.1.5
### Features
- **Plugin support**: Added the ability to load Claude Code plugins programmatically through the SDK. Plugins can be specified using the new `plugins` field in `ClaudeAgentOptions` with a `SdkPluginConfig` type that supports loading local plugins by path. This enables SDK applications to extend functionality with custom commands and capabilities defined in plugin directories
## 0.1.4
### Features
- **Skip version check**: Added `CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK` environment variable to allow users to disable the Claude Code version check. Set this environment variable to skip the minimum version validation when the SDK connects to Claude Code. (Only recommended if you already have Claude Code 2.0.0 or higher installed, otherwise some functionality may break)
- SDK MCP server tool calls can now return image content blocks
## 0.1.3
### Features
- **Strongly-typed hook inputs**: Added typed hook input structures (`PreToolUseHookInput`, `PostToolUseHookInput`, `UserPromptSubmitHookInput`, etc.) using TypedDict for better IDE autocomplete and type safety. Hook callbacks now receive fully typed input parameters
### Bug Fixes
- **Hook output field conversion**: Fixed bug where Python-safe field names (`async_`, `continue_`) in hook outputs were not being converted to CLI format (`async`, `continue`). This caused hook control fields to be silently ignored, preventing proper hook behavior. The SDK now automatically converts field names when communicating with the CLI
### Internal/Other Changes
- **CI/CD**: Re-enabled Windows testing in the end-to-end test workflow. Windows CI had been temporarily disabled but is now fully operational across all test suites
## 0.1.2
### Bug Fixes
- **Hook output fields**: Added missing hook output fields to match the TypeScript SDK, including `reason`, `continue_`, `suppressOutput`, and `stopReason`. The `decision` field now properly supports both "approve" and "block" values. Added `AsyncHookJSONOutput` type for deferred hook execution and proper typing for `hookSpecificOutput` with discriminated unions
## 0.1.1
### Features
- **Minimum Claude Code version check**: Added version validation to ensure Claude Code 2.0.0+ is installed. The SDK will display a warning if an older version is detected, helping prevent compatibility issues
- **Updated PermissionResult types**: Aligned permission result types with the latest control protocol for better type safety and compatibility
### Improvements
- **Model references**: Updated all examples and tests to use the simplified `claude-sonnet-4-5` model identifier instead of dated version strings
## 0.1.0
Introducing the Claude Agent SDK! The Claude Code SDK has been renamed to better reflect its capabilities for building AI agents across all domains, not just coding.
### Breaking Changes
#### Type Name Changes
- **ClaudeCodeOptions renamed to ClaudeAgentOptions**: The options type has been renamed to match the new SDK branding. Update all imports and type references:
```python
# Before
from claude_agent_sdk import query, ClaudeCodeOptions
options = ClaudeCodeOptions(...)
# After
from claude_agent_sdk import query, ClaudeAgentOptions
options = ClaudeAgentOptions(...)
```
#### System Prompt Changes
- **Merged prompt options**: The `custom_system_prompt` and `append_system_prompt` fields have been merged into a single `system_prompt` field for simpler configuration
- **No default system prompt**: The Claude Code system prompt is no longer included by default, giving you full control over agent behavior. To use the Claude Code system prompt, explicitly set:
```python
system_prompt={"type": "preset", "preset": "claude_code"}
```
#### Settings Isolation
- **No filesystem settings by default**: Settings files (`settings.json`, `CLAUDE.md`), slash commands, and subagents are no longer loaded automatically. This ensures SDK applications have predictable behavior independent of local filesystem configurations
- **Explicit settings control**: Use the new `setting_sources` field to specify which settings locations to load: `["user", "project", "local"]`
For full migration instructions, see our [migration guide](https://docs.claude.com/en/docs/claude-code/sdk/migration-guide).
### New Features
- **Programmatic subagents**: Subagents can now be defined inline in code using the `agents` option, enabling dynamic agent creation without filesystem dependencies. [Learn more](https://docs.claude.com/en/api/agent-sdk/subagents)
- **Session forking**: Resume sessions with the new `fork_session` option to branch conversations and explore different approaches from the same starting point. [Learn more](https://docs.claude.com/en/api/agent-sdk/sessions)
- **Granular settings control**: The `setting_sources` option gives you fine-grained control over which filesystem settings to load, improving isolation for CI/CD, testing, and production deployments
### Documentation
- Comprehensive documentation now available in the [API Guide](https://docs.claude.com/en/api/agent-sdk/overview)
- New guides for [Custom Tools](https://docs.claude.com/en/api/agent-sdk/custom-tools), [Permissions](https://docs.claude.com/en/api/agent-sdk/permissions), [Session Management](https://docs.claude.com/en/api/agent-sdk/sessions), and more
- Complete [Python API reference](https://docs.claude.com/en/api/agent-sdk/python)
## 0.0.22 ## 0.0.22
- Introduce custom tools, implemented as in-process MCP servers. - Introduce custom tools, implemented as in-process MCP servers.
@ -37,4 +236,3 @@
- Fix multi-line buffering issue - Fix multi-line buffering issue
- Rename cost_usd to total_cost_usd in API responses - Rename cost_usd to total_cost_usd in API responses
- Fix optional cost fields handling - Fix optional cost fields handling

View file

@ -18,7 +18,7 @@ python -m pytest tests/test_client.py
# Codebase Structure # Codebase Structure
- `src/claude_code_sdk/` - Main package - `src/claude_agent_sdk/` - Main package
- `client.py` - ClaudeSDKClient for interactive sessions - `client.py` - ClaudeSDKClient for interactive sessions
- `query.py` - One-shot query function - `query.py` - One-shot query function
- `types.py` - Type definitions - `types.py` - Type definitions

29
Dockerfile.test Normal file
View file

@ -0,0 +1,29 @@
# Dockerfile for running SDK tests in a containerized environment
# This helps catch Docker-specific issues like #406
FROM python:3.12-slim
# Install dependencies for Claude CLI and git (needed for some tests)
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Install Claude Code CLI
RUN curl -fsSL https://claude.ai/install.sh | bash
ENV PATH="/root/.local/bin:$PATH"
# Set up working directory
WORKDIR /app
# Copy the SDK source
COPY . .
# Install SDK with dev dependencies
RUN pip install -e ".[dev]"
# Verify CLI installation
RUN claude -v
# Default: run unit tests
CMD ["python", "-m", "pytest", "tests/", "-v"]

136
README.md
View file

@ -1,23 +1,27 @@
# Claude Code SDK for Python # Claude Agent SDK for Python
Python SDK for Claude Code. See the [Claude Code SDK documentation](https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-python) for more information. Python SDK for Claude Agent. See the [Claude Agent SDK documentation](https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-python) for more information.
## Installation ## Installation
```bash ```bash
pip install claude-code-sdk pip install claude-agent-sdk
``` ```
**Prerequisites:** **Prerequisites:**
- Python 3.10+ - Python 3.10+
- Node.js
- Claude Code: `npm install -g @anthropic-ai/claude-code` **Note:** The Claude Code CLI is automatically bundled with the package - no separate installation required! The SDK will use the bundled CLI by default. If you prefer to use a system-wide installation or a specific version, you can:
- Install Claude Code separately: `curl -fsSL https://claude.ai/install.sh | bash`
- Specify a custom path: `ClaudeAgentOptions(cli_path="/path/to/claude")`
## Quick Start ## Quick Start
```python ```python
import anyio import anyio
from claude_code_sdk import query from claude_agent_sdk import query
async def main(): async def main():
async for message in query(prompt="What is 2 + 2?"): async for message in query(prompt="What is 2 + 2?"):
@ -28,10 +32,10 @@ anyio.run(main)
## Basic Usage: query() ## Basic Usage: query()
`query()` is an async function for querying Claude Code. It returns an `AsyncIterator` of response messages. See [src/claude_code_sdk/query.py](src/claude_code_sdk/query.py). `query()` is an async function for querying Claude Code. It returns an `AsyncIterator` of response messages. See [src/claude_agent_sdk/query.py](src/claude_agent_sdk/query.py).
```python ```python
from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage, TextBlock from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock
# Simple query # Simple query
async for message in query(prompt="Hello Claude"): async for message in query(prompt="Hello Claude"):
@ -41,7 +45,7 @@ async for message in query(prompt="Hello Claude"):
print(block.text) print(block.text)
# With options # With options
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
system_prompt="You are a helpful assistant", system_prompt="You are a helpful assistant",
max_turns=1 max_turns=1
) )
@ -53,13 +57,13 @@ async for message in query(prompt="Tell me a joke", options=options):
### Using Tools ### Using Tools
```python ```python
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Bash"], allowed_tools=["Read", "Write", "Bash"],
permission_mode='acceptEdits' # auto-accept file edits permission_mode='acceptEdits' # auto-accept file edits
) )
async for message in query( async for message in query(
prompt="Create a hello.py file", prompt="Create a hello.py file",
options=options options=options
): ):
# Process tool use and results # Process tool use and results
@ -71,7 +75,7 @@ async for message in query(
```python ```python
from pathlib import Path from pathlib import Path
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
cwd="/path/to/project" # or Path("/path/to/project") cwd="/path/to/project" # or Path("/path/to/project")
) )
``` ```
@ -79,7 +83,7 @@ options = ClaudeCodeOptions(
## ClaudeSDKClient ## ClaudeSDKClient
`ClaudeSDKClient` supports bidirectional, interactive conversations with Claude `ClaudeSDKClient` supports bidirectional, interactive conversations with Claude
Code. See [src/claude_code_sdk/client.py](src/claude_code_sdk/client.py). Code. See [src/claude_agent_sdk/client.py](src/claude_agent_sdk/client.py).
Unlike `query()`, `ClaudeSDKClient` additionally enables **custom tools** and **hooks**, both of which can be defined as Python functions. Unlike `query()`, `ClaudeSDKClient` additionally enables **custom tools** and **hooks**, both of which can be defined as Python functions.
@ -94,7 +98,7 @@ For an end-to-end example, see [MCP Calculator](examples/mcp_calculator.py).
#### Creating a Simple Tool #### Creating a Simple Tool
```python ```python
from claude_code_sdk import tool, create_sdk_mcp_server, ClaudeCodeOptions, ClaudeSDKClient from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeAgentOptions, ClaudeSDKClient
# Define a tool using the @tool decorator # Define a tool using the @tool decorator
@tool("greet", "Greet a user", {"name": str}) @tool("greet", "Greet a user", {"name": str})
@ -113,7 +117,7 @@ server = create_sdk_mcp_server(
) )
# Use it with Claude # Use it with Claude
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
mcp_servers={"tools": server}, mcp_servers={"tools": server},
allowed_tools=["mcp__tools__greet"] allowed_tools=["mcp__tools__greet"]
) )
@ -138,7 +142,7 @@ async with ClaudeSDKClient(options=options) as client:
```python ```python
# BEFORE: External MCP server (separate process) # BEFORE: External MCP server (separate process)
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
mcp_servers={ mcp_servers={
"calculator": { "calculator": {
"type": "stdio", "type": "stdio",
@ -156,7 +160,7 @@ calculator = create_sdk_mcp_server(
tools=[add, subtract] tools=[add, subtract]
) )
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
mcp_servers={"calculator": calculator} mcp_servers={"calculator": calculator}
) )
``` ```
@ -166,7 +170,7 @@ options = ClaudeCodeOptions(
You can use both SDK and external MCP servers together: You can use both SDK and external MCP servers together:
```python ```python
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
mcp_servers={ mcp_servers={
"internal": sdk_server, # In-process SDK server "internal": sdk_server, # In-process SDK server
"external": { # External subprocess server "external": { # External subprocess server
@ -179,14 +183,14 @@ options = ClaudeCodeOptions(
### Hooks ### Hooks
A **hook** is a Python function that the Claude Code *application* (*not* Claude) invokes at specific points of the Claude agent loop. Hooks can provide deterministic processing and automated feedback for Claude. Read more in [Claude Code Hooks Reference](https://docs.anthropic.com/en/docs/claude-code/hooks). A **hook** is a Python function that the Claude Code _application_ (_not_ Claude) invokes at specific points of the Claude agent loop. Hooks can provide deterministic processing and automated feedback for Claude. Read more in [Claude Code Hooks Reference](https://docs.anthropic.com/en/docs/claude-code/hooks).
For more examples, see examples/hooks.py. For more examples, see examples/hooks.py.
#### Example #### Example
```python ```python
from claude_code_sdk import ClaudeCodeOptions, ClaudeSDKClient, HookMatcher from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher
async def check_bash_command(input_data, tool_use_id, context): async def check_bash_command(input_data, tool_use_id, context):
tool_name = input_data["tool_name"] tool_name = input_data["tool_name"]
@ -206,7 +210,7 @@ async def check_bash_command(input_data, tool_use_id, context):
} }
return {} return {}
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
allowed_tools=["Bash"], allowed_tools=["Bash"],
hooks={ hooks={
"PreToolUse": [ "PreToolUse": [
@ -229,18 +233,18 @@ async with ClaudeSDKClient(options=options) as client:
print(msg) print(msg)
``` ```
## Types ## Types
See [src/claude_code_sdk/types.py](src/claude_code_sdk/types.py) for complete type definitions: See [src/claude_agent_sdk/types.py](src/claude_agent_sdk/types.py) for complete type definitions:
- `ClaudeCodeOptions` - Configuration options
- `ClaudeAgentOptions` - Configuration options
- `AssistantMessage`, `UserMessage`, `SystemMessage`, `ResultMessage` - Message types - `AssistantMessage`, `UserMessage`, `SystemMessage`, `ResultMessage` - Message types
- `TextBlock`, `ToolUseBlock`, `ToolResultBlock` - Content blocks - `TextBlock`, `ToolUseBlock`, `ToolResultBlock` - Content blocks
## Error Handling ## Error Handling
```python ```python
from claude_code_sdk import ( from claude_agent_sdk import (
ClaudeSDKError, # Base error ClaudeSDKError, # Base error
CLINotFoundError, # Claude Code not installed CLINotFoundError, # Claude Code not installed
CLIConnectionError, # Connection issues CLIConnectionError, # Connection issues
@ -259,7 +263,7 @@ except CLIJSONDecodeError as e:
print(f"Failed to parse response: {e}") print(f"Failed to parse response: {e}")
``` ```
See [src/claude_code_sdk/_errors.py](src/claude_code_sdk/_errors.py) for all error types. See [src/claude_agent_sdk/\_errors.py](src/claude_agent_sdk/_errors.py) for all error types.
## Available Tools ## Available Tools
@ -271,6 +275,82 @@ See [examples/quick_start.py](examples/quick_start.py) for a complete working ex
See [examples/streaming_mode.py](examples/streaming_mode.py) for comprehensive examples involving `ClaudeSDKClient`. You can even run interactive examples in IPython from [examples/streaming_mode_ipython.py](examples/streaming_mode_ipython.py). See [examples/streaming_mode.py](examples/streaming_mode.py) for comprehensive examples involving `ClaudeSDKClient`. You can even run interactive examples in IPython from [examples/streaming_mode_ipython.py](examples/streaming_mode_ipython.py).
## License ## Migrating from Claude Code SDK
MIT If you're upgrading from the Claude Code SDK (versions < 0.1.0), please see the [CHANGELOG.md](CHANGELOG.md#010) for details on breaking changes and new features, including:
- `ClaudeCodeOptions``ClaudeAgentOptions` rename
- Merged system prompt configuration
- Settings isolation and explicit control
- New programmatic subagents and session forking features
## Development
If you're contributing to this project, run the initial setup script to install git hooks:
```bash
./scripts/initial-setup.sh
```
This installs a pre-push hook that runs lint checks before pushing, matching the CI workflow. To skip the hook temporarily, use `git push --no-verify`.
### Building Wheels Locally
To build wheels with the bundled Claude Code CLI:
```bash
# Install build dependencies
pip install build twine
# Build wheel with bundled CLI
python scripts/build_wheel.py
# Build with specific version
python scripts/build_wheel.py --version 0.1.4
# Build with specific CLI version
python scripts/build_wheel.py --cli-version 2.0.0
# Clean bundled CLI after building
python scripts/build_wheel.py --clean
# Skip CLI download (use existing)
python scripts/build_wheel.py --skip-download
```
The build script:
1. Downloads Claude Code CLI for your platform
2. Bundles it in the wheel
3. Builds both wheel and source distribution
4. Checks the package with twine
See `python scripts/build_wheel.py --help` for all options.
### Release Workflow
The package is published to PyPI via the GitHub Actions workflow in `.github/workflows/publish.yml`. To create a new release:
1. **Trigger the workflow** manually from the Actions tab with two inputs:
- `version`: The package version to publish (e.g., `0.1.5`)
- `claude_code_version`: The Claude Code CLI version to bundle (e.g., `2.0.0` or `latest`)
2. **The workflow will**:
- Build platform-specific wheels for macOS, Linux, and Windows
- Bundle the specified Claude Code CLI version in each wheel
- Build a source distribution
- Publish all artifacts to PyPI
- Create a release branch with version updates
- Open a PR to main with:
- Updated `pyproject.toml` version
- Updated `src/claude_agent_sdk/_version.py`
- Updated `src/claude_agent_sdk/_cli_version.py` with bundled CLI version
- Auto-generated `CHANGELOG.md` entry
3. **Review and merge** the release PR to update main with the new version information
The workflow tracks both the package version and the bundled CLI version separately, allowing you to release a new package version with an updated CLI without code changes.
## License and terms
Use of this SDK is governed by Anthropic's [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms), including when you use it to power products and services that you make available to your own customers and end users, except to the extent a specific component or dependency is covered by a different license as indicated in that component's LICENSE file.

View file

@ -0,0 +1,242 @@
"""End-to-end tests for agents and setting sources with real Claude API calls."""
import asyncio
import sys
import tempfile
from pathlib import Path
import pytest
from claude_agent_sdk import (
AgentDefinition,
ClaudeAgentOptions,
ClaudeSDKClient,
SystemMessage,
)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_agent_definition():
"""Test that custom agent definitions work."""
options = ClaudeAgentOptions(
agents={
"test-agent": AgentDefinition(
description="A test agent for verification",
prompt="You are a test agent. Always respond with 'Test agent activated'",
tools=["Read"],
model="sonnet",
)
},
max_turns=1,
)
async with ClaudeSDKClient(options=options) as client:
await client.query("What is 2 + 2?")
# Check that agent is available in init message
async for message in client.receive_response():
if isinstance(message, SystemMessage) and message.subtype == "init":
agents = message.data.get("agents", [])
assert isinstance(agents, list), (
f"agents should be a list of strings, got: {type(agents)}"
)
assert "test-agent" in agents, (
f"test-agent should be available, got: {agents}"
)
break
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_filesystem_agent_loading():
"""Test that filesystem-based agents load via setting_sources and produce full response.
This is the core test for issue #406. It verifies that when using
setting_sources=["project"] with a .claude/agents/ directory containing
agent definitions, the SDK:
1. Loads the agents (they appear in init message)
2. Produces a full response with AssistantMessage
3. Completes with a ResultMessage
The bug in #406 causes the iterator to complete after only the
init SystemMessage, never yielding AssistantMessage or ResultMessage.
"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create a temporary project with a filesystem agent
project_dir = Path(tmpdir)
agents_dir = project_dir / ".claude" / "agents"
agents_dir.mkdir(parents=True)
# Create a test agent file
agent_file = agents_dir / "fs-test-agent.md"
agent_file.write_text(
"""---
name: fs-test-agent
description: A filesystem test agent for SDK testing
tools: Read
---
# Filesystem Test Agent
You are a simple test agent. When asked a question, provide a brief, helpful answer.
"""
)
options = ClaudeAgentOptions(
setting_sources=["project"],
cwd=project_dir,
max_turns=1,
)
messages = []
async with ClaudeSDKClient(options=options) as client:
await client.query("Say hello in exactly 3 words")
async for msg in client.receive_response():
messages.append(msg)
# Must have at least init, assistant, result
message_types = [type(m).__name__ for m in messages]
assert "SystemMessage" in message_types, "Missing SystemMessage (init)"
assert "AssistantMessage" in message_types, (
f"Missing AssistantMessage - got only: {message_types}. "
"This may indicate issue #406 (silent failure with filesystem agents)."
)
assert "ResultMessage" in message_types, "Missing ResultMessage"
# Find the init message and check for the filesystem agent
for msg in messages:
if isinstance(msg, SystemMessage) and msg.subtype == "init":
agents = msg.data.get("agents", [])
# Agents are returned as strings (just names)
assert "fs-test-agent" in agents, (
f"fs-test-agent not loaded from filesystem. Found: {agents}"
)
break
# On Windows, wait for file handles to be released before cleanup
if sys.platform == "win32":
await asyncio.sleep(0.5)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_setting_sources_default():
"""Test that default (no setting_sources) loads no settings."""
with tempfile.TemporaryDirectory() as tmpdir:
# Create a temporary project with local settings
project_dir = Path(tmpdir)
claude_dir = project_dir / ".claude"
claude_dir.mkdir(parents=True)
# Create local settings with custom outputStyle
settings_file = claude_dir / "settings.local.json"
settings_file.write_text('{"outputStyle": "local-test-style"}')
# Don't provide setting_sources - should default to no settings
options = ClaudeAgentOptions(
cwd=project_dir,
max_turns=1,
)
async with ClaudeSDKClient(options=options) as client:
await client.query("What is 2 + 2?")
# Check that settings were NOT loaded
async for message in client.receive_response():
if isinstance(message, SystemMessage) and message.subtype == "init":
output_style = message.data.get("output_style")
assert output_style != "local-test-style", (
f"outputStyle should NOT be from local settings (default is no settings), got: {output_style}"
)
assert output_style == "default", (
f"outputStyle should be 'default', got: {output_style}"
)
break
# On Windows, wait for file handles to be released before cleanup
if sys.platform == "win32":
await asyncio.sleep(0.5)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_setting_sources_user_only():
"""Test that setting_sources=['user'] excludes project settings."""
with tempfile.TemporaryDirectory() as tmpdir:
# Create a temporary project with a slash command
project_dir = Path(tmpdir)
commands_dir = project_dir / ".claude" / "commands"
commands_dir.mkdir(parents=True)
test_command = commands_dir / "testcmd.md"
test_command.write_text(
"""---
description: Test command
---
This is a test command.
"""
)
# Use setting_sources=["user"] to exclude project settings
options = ClaudeAgentOptions(
setting_sources=["user"],
cwd=project_dir,
max_turns=1,
)
async with ClaudeSDKClient(options=options) as client:
await client.query("What is 2 + 2?")
# Check that project command is NOT available
async for message in client.receive_response():
if isinstance(message, SystemMessage) and message.subtype == "init":
commands = message.data.get("slash_commands", [])
assert "testcmd" not in commands, (
f"testcmd should NOT be available with user-only sources, got: {commands}"
)
break
# On Windows, wait for file handles to be released before cleanup
if sys.platform == "win32":
await asyncio.sleep(0.5)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_setting_sources_project_included():
"""Test that setting_sources=['user', 'project'] includes project settings."""
with tempfile.TemporaryDirectory() as tmpdir:
# Create a temporary project with local settings
project_dir = Path(tmpdir)
claude_dir = project_dir / ".claude"
claude_dir.mkdir(parents=True)
# Create local settings with custom outputStyle
settings_file = claude_dir / "settings.local.json"
settings_file.write_text('{"outputStyle": "local-test-style"}')
# Use setting_sources=["user", "project", "local"] to include local settings
options = ClaudeAgentOptions(
setting_sources=["user", "project", "local"],
cwd=project_dir,
max_turns=1,
)
async with ClaudeSDKClient(options=options) as client:
await client.query("What is 2 + 2?")
# Check that settings WERE loaded
async for message in client.receive_response():
if isinstance(message, SystemMessage) and message.subtype == "init":
output_style = message.data.get("output_style")
assert output_style == "local-test-style", (
f"outputStyle should be from local settings, got: {output_style}"
)
break
# On Windows, wait for file handles to be released before cleanup
if sys.platform == "win32":
await asyncio.sleep(0.5)

View file

@ -0,0 +1,97 @@
"""End-to-end tests for dynamic control features with real Claude API calls."""
import pytest
from claude_agent_sdk import (
ClaudeAgentOptions,
ClaudeSDKClient,
)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_set_permission_mode():
"""Test that permission mode can be changed dynamically during a session."""
options = ClaudeAgentOptions(
permission_mode="default",
)
async with ClaudeSDKClient(options=options) as client:
# Change permission mode to acceptEdits
await client.set_permission_mode("acceptEdits")
# Make a query that would normally require permission
await client.query("What is 2+2? Just respond with the number.")
async for message in client.receive_response():
print(f"Got message: {message}")
pass # Just consume messages
# Change back to default
await client.set_permission_mode("default")
# Make another query
await client.query("What is 3+3? Just respond with the number.")
async for message in client.receive_response():
print(f"Got message: {message}")
pass # Just consume messages
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_set_model():
"""Test that model can be changed dynamically during a session."""
options = ClaudeAgentOptions()
async with ClaudeSDKClient(options=options) as client:
# Start with default model
await client.query("What is 1+1? Just the number.")
async for message in client.receive_response():
print(f"Default model response: {message}")
pass
# Switch to Haiku model
await client.set_model("claude-3-5-haiku-20241022")
await client.query("What is 2+2? Just the number.")
async for message in client.receive_response():
print(f"Haiku model response: {message}")
pass
# Switch back to default (None means default)
await client.set_model(None)
await client.query("What is 3+3? Just the number.")
async for message in client.receive_response():
print(f"Back to default model: {message}")
pass
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_interrupt():
"""Test that interrupt can be sent during a session."""
options = ClaudeAgentOptions()
async with ClaudeSDKClient(options=options) as client:
# Start a query
await client.query("Count from 1 to 100 slowly.")
# Send interrupt (may or may not stop the response depending on timing)
try:
await client.interrupt()
print("Interrupt sent successfully")
except Exception as e:
print(f"Interrupt resulted in: {e}")
# Consume any remaining messages
async for message in client.receive_response():
print(f"Got message after interrupt: {message}")
pass

150
e2e-tests/test_hooks.py Normal file
View file

@ -0,0 +1,150 @@
"""End-to-end tests for hook callbacks with real Claude API calls."""
import pytest
from claude_agent_sdk import (
ClaudeAgentOptions,
ClaudeSDKClient,
HookContext,
HookInput,
HookJSONOutput,
HookMatcher,
)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_hook_with_permission_decision_and_reason():
"""Test that hooks with permissionDecision and reason fields work end-to-end."""
hook_invocations = []
async def test_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Hook that uses permissionDecision and reason fields."""
tool_name = input_data.get("tool_name", "")
print(f"Hook called for tool: {tool_name}")
hook_invocations.append(tool_name)
# Block Bash commands for this test
if tool_name == "Bash":
return {
"reason": "Bash commands are blocked in this test for safety",
"systemMessage": "⚠️ Command blocked by hook",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Security policy: Bash blocked",
},
}
return {
"reason": "Tool approved by security review",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Tool passed security checks",
},
}
options = ClaudeAgentOptions(
allowed_tools=["Bash", "Write"],
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[test_hook]),
],
},
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Run this bash command: echo 'hello'")
async for message in client.receive_response():
print(f"Got message: {message}")
print(f"Hook invocations: {hook_invocations}")
# Verify hook was called
assert "Bash" in hook_invocations, f"Hook should have been invoked for Bash tool, got: {hook_invocations}"
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_hook_with_continue_and_stop_reason():
"""Test that hooks with continue_=False and stopReason fields work end-to-end."""
hook_invocations = []
async def post_tool_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""PostToolUse hook that stops execution with stopReason."""
tool_name = input_data.get("tool_name", "")
hook_invocations.append(tool_name)
# Actually test continue_=False and stopReason fields
return {
"continue_": False,
"stopReason": "Execution halted by test hook for validation",
"reason": "Testing continue and stopReason fields",
"systemMessage": "🛑 Test hook stopped execution",
}
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
hooks={
"PostToolUse": [
HookMatcher(matcher="Bash", hooks=[post_tool_hook]),
],
},
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Run: echo 'test message'")
async for message in client.receive_response():
print(f"Got message: {message}")
print(f"Hook invocations: {hook_invocations}")
# Verify hook was called
assert "Bash" in hook_invocations, f"PostToolUse hook should have been invoked, got: {hook_invocations}"
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_hook_with_additional_context():
"""Test that hooks with hookSpecificOutput work end-to-end."""
hook_invocations = []
async def context_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Hook that provides additional context."""
hook_invocations.append("context_added")
return {
"systemMessage": "Additional context provided by hook",
"reason": "Hook providing monitoring feedback",
"suppressOutput": False,
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "The command executed successfully with hook monitoring",
},
}
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
hooks={
"PostToolUse": [
HookMatcher(matcher="Bash", hooks=[context_hook]),
],
},
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Run: echo 'testing hooks'")
async for message in client.receive_response():
print(f"Got message: {message}")
print(f"Hook invocations: {hook_invocations}")
# Verify hook was called
assert "context_added" in hook_invocations, "Hook with hookSpecificOutput should have been invoked"

View file

@ -9,9 +9,9 @@ from typing import List, Any
import pytest import pytest
from claude_code_sdk import ClaudeSDKClient from claude_agent_sdk import ClaudeSDKClient
from claude_code_sdk.types import ( from claude_agent_sdk.types import (
ClaudeCodeOptions, ClaudeAgentOptions,
StreamEvent, StreamEvent,
AssistantMessage, AssistantMessage,
SystemMessage, SystemMessage,
@ -26,9 +26,9 @@ from claude_code_sdk.types import (
async def test_include_partial_messages_stream_events(): async def test_include_partial_messages_stream_events():
"""Test that include_partial_messages produces StreamEvent messages.""" """Test that include_partial_messages produces StreamEvent messages."""
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
include_partial_messages=True, include_partial_messages=True,
model="claude-sonnet-4-20250514", model="claude-sonnet-4-5",
max_turns=2, max_turns=2,
env={ env={
"MAX_THINKING_TOKENS": "8000", "MAX_THINKING_TOKENS": "8000",
@ -92,9 +92,9 @@ async def test_include_partial_messages_stream_events():
async def test_include_partial_messages_thinking_deltas(): async def test_include_partial_messages_thinking_deltas():
"""Test that thinking content is streamed incrementally via deltas.""" """Test that thinking content is streamed incrementally via deltas."""
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
include_partial_messages=True, include_partial_messages=True,
model="claude-sonnet-4-20250514", model="claude-sonnet-4-5",
max_turns=2, max_turns=2,
env={ env={
"MAX_THINKING_TOKENS": "8000", "MAX_THINKING_TOKENS": "8000",
@ -130,9 +130,9 @@ async def test_include_partial_messages_thinking_deltas():
async def test_partial_messages_disabled_by_default(): async def test_partial_messages_disabled_by_default():
"""Test that partial messages are not included when option is not set.""" """Test that partial messages are not included when option is not set."""
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
# include_partial_messages not set (defaults to False) # include_partial_messages not set (defaults to False)
model="claude-sonnet-4-20250514", model="claude-sonnet-4-5",
max_turns=2, max_turns=2,
) )

View file

@ -8,8 +8,8 @@ from typing import Any
import pytest import pytest
from claude_code_sdk import ( from claude_agent_sdk import (
ClaudeCodeOptions, ClaudeAgentOptions,
ClaudeSDKClient, ClaudeSDKClient,
create_sdk_mcp_server, create_sdk_mcp_server,
tool, tool,
@ -34,7 +34,7 @@ async def test_sdk_mcp_tool_execution():
tools=[echo_tool], tools=[echo_tool],
) )
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
mcp_servers={"test": server}, mcp_servers={"test": server},
allowed_tools=["mcp__test__echo"], allowed_tools=["mcp__test__echo"],
) )
@ -73,7 +73,7 @@ async def test_sdk_mcp_permission_enforcement():
tools=[echo_tool, greet_tool], tools=[echo_tool, greet_tool],
) )
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
mcp_servers={"test": server}, mcp_servers={"test": server},
disallowed_tools=["mcp__test__echo"], # Block echo tool disallowed_tools=["mcp__test__echo"], # Block echo tool
allowed_tools=["mcp__test__greet"], # But allow greet allowed_tools=["mcp__test__greet"], # But allow greet
@ -116,7 +116,7 @@ async def test_sdk_mcp_multiple_tools():
tools=[echo_tool, greet_tool], tools=[echo_tool, greet_tool],
) )
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
mcp_servers={"multi": server}, mcp_servers={"multi": server},
allowed_tools=["mcp__multi__echo", "mcp__multi__greet"], allowed_tools=["mcp__multi__echo", "mcp__multi__greet"],
) )
@ -153,7 +153,7 @@ async def test_sdk_mcp_without_permissions():
) )
# No allowed_tools specified # No allowed_tools specified
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
mcp_servers={"noperm": server}, mcp_servers={"noperm": server},
) )

View file

@ -0,0 +1,49 @@
"""End-to-end test for stderr callback functionality."""
import pytest
from claude_agent_sdk import ClaudeAgentOptions, query
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_stderr_callback_captures_debug_output():
"""Test that stderr callback receives debug output when enabled."""
stderr_lines = []
def capture_stderr(line: str):
stderr_lines.append(line)
# Enable debug mode to generate stderr output
options = ClaudeAgentOptions(
stderr=capture_stderr,
extra_args={"debug-to-stderr": None}
)
# Run a simple query
async for _ in query(prompt="What is 1+1?", options=options):
pass # Just consume messages
# Verify we captured debug output
assert len(stderr_lines) > 0, "Should capture stderr output with debug enabled"
assert any("[DEBUG]" in line for line in stderr_lines), "Should contain DEBUG messages"
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_stderr_callback_without_debug():
"""Test that stderr callback works but receives no output without debug mode."""
stderr_lines = []
def capture_stderr(line: str):
stderr_lines.append(line)
# No debug mode enabled
options = ClaudeAgentOptions(stderr=capture_stderr)
# Run a simple query
async for _ in query(prompt="What is 1+1?", options=options):
pass # Just consume messages
# Should work but capture minimal/no output without debug
assert len(stderr_lines) == 0, "Should not capture stderr output without debug mode"

View file

@ -0,0 +1,204 @@
"""End-to-end tests for structured output with real Claude API calls.
These tests verify that the output_schema feature works correctly by making
actual API calls to Claude with JSON Schema validation.
"""
import tempfile
import pytest
from claude_agent_sdk import (
ClaudeAgentOptions,
ResultMessage,
query,
)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_simple_structured_output():
"""Test structured output with file counting requiring tool use."""
# Define schema for file analysis
schema = {
"type": "object",
"properties": {
"file_count": {"type": "number"},
"has_tests": {"type": "boolean"},
"test_file_count": {"type": "number"},
},
"required": ["file_count", "has_tests"],
}
options = ClaudeAgentOptions(
output_format={"type": "json_schema", "schema": schema},
permission_mode="acceptEdits",
cwd=".", # Use current directory
)
# Agent must use Glob/Bash to count files
result_message = None
async for message in query(
prompt="Count how many Python files are in src/claude_agent_sdk/ and check if there are any test files. Use tools to explore the filesystem.",
options=options,
):
if isinstance(message, ResultMessage):
result_message = message
# Verify result
assert result_message is not None, "No result message received"
assert not result_message.is_error, f"Query failed: {result_message.result}"
assert result_message.subtype == "success"
# Verify structured output is present and valid
assert result_message.structured_output is not None, "No structured output in result"
assert "file_count" in result_message.structured_output
assert "has_tests" in result_message.structured_output
assert isinstance(result_message.structured_output["file_count"], (int, float))
assert isinstance(result_message.structured_output["has_tests"], bool)
# Should find Python files in src/
assert result_message.structured_output["file_count"] > 0
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_nested_structured_output():
"""Test structured output with nested objects and arrays."""
# Define a schema with nested structure
schema = {
"type": "object",
"properties": {
"analysis": {
"type": "object",
"properties": {
"word_count": {"type": "number"},
"character_count": {"type": "number"},
},
"required": ["word_count", "character_count"],
},
"words": {
"type": "array",
"items": {"type": "string"},
},
},
"required": ["analysis", "words"],
}
options = ClaudeAgentOptions(
output_format={"type": "json_schema", "schema": schema},
permission_mode="acceptEdits",
)
result_message = None
async for message in query(
prompt="Analyze this text: 'Hello world'. Provide word count, character count, and list of words.",
options=options,
):
if isinstance(message, ResultMessage):
result_message = message
# Verify result
assert result_message is not None
assert not result_message.is_error
assert result_message.structured_output is not None
# Check nested structure
output = result_message.structured_output
assert "analysis" in output
assert "words" in output
assert output["analysis"]["word_count"] == 2
assert output["analysis"]["character_count"] == 11 # "Hello world"
assert len(output["words"]) == 2
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_structured_output_with_enum():
"""Test structured output with enum constraints requiring code analysis."""
schema = {
"type": "object",
"properties": {
"has_tests": {"type": "boolean"},
"test_framework": {
"type": "string",
"enum": ["pytest", "unittest", "nose", "unknown"],
},
"test_count": {"type": "number"},
},
"required": ["has_tests", "test_framework"],
}
options = ClaudeAgentOptions(
output_format={"type": "json_schema", "schema": schema},
permission_mode="acceptEdits",
cwd=".",
)
result_message = None
async for message in query(
prompt="Search for test files in the tests/ directory. Determine which test framework is being used (pytest/unittest/nose) and count how many test files exist. Use Grep to search for framework imports.",
options=options,
):
if isinstance(message, ResultMessage):
result_message = message
# Verify result
assert result_message is not None
assert not result_message.is_error
assert result_message.structured_output is not None
# Check enum values are valid
output = result_message.structured_output
assert output["test_framework"] in ["pytest", "unittest", "nose", "unknown"]
assert isinstance(output["has_tests"], bool)
# This repo uses pytest
assert output["has_tests"] is True
assert output["test_framework"] == "pytest"
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_structured_output_with_tools():
"""Test structured output when agent uses tools."""
# Schema for file analysis
schema = {
"type": "object",
"properties": {
"file_count": {"type": "number"},
"has_readme": {"type": "boolean"},
},
"required": ["file_count", "has_readme"],
}
options = ClaudeAgentOptions(
output_format={"type": "json_schema", "schema": schema},
permission_mode="acceptEdits",
cwd=tempfile.gettempdir(), # Cross-platform temp directory
)
result_message = None
async for message in query(
prompt="Count how many files are in the current directory and check if there's a README file. Use tools as needed.",
options=options,
):
if isinstance(message, ResultMessage):
result_message = message
# Verify result
assert result_message is not None
assert not result_message.is_error
assert result_message.structured_output is not None
# Check structure
output = result_message.structured_output
assert "file_count" in output
assert "has_readme" in output
assert isinstance(output["file_count"], (int, float))
assert isinstance(output["has_readme"], bool)
assert output["file_count"] >= 0 # Should be non-negative

View file

@ -2,8 +2,8 @@
import pytest import pytest
from claude_code_sdk import ( from claude_agent_sdk import (
ClaudeCodeOptions, ClaudeAgentOptions,
ClaudeSDKClient, ClaudeSDKClient,
PermissionResultAllow, PermissionResultAllow,
PermissionResultDeny, PermissionResultDeny,
@ -27,7 +27,7 @@ async def test_permission_callback_gets_called():
callback_invocations.append(tool_name) callback_invocations.append(tool_name)
return PermissionResultAllow() return PermissionResultAllow()
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
can_use_tool=permission_callback, can_use_tool=permission_callback,
) )

124
examples/agents.py Normal file
View file

@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Example of using custom agents with Claude Code SDK.
This example demonstrates how to define and use custom agents with specific
tools, prompts, and models.
Usage:
./examples/agents.py - Run the example
"""
import anyio
from claude_agent_sdk import (
AgentDefinition,
AssistantMessage,
ClaudeAgentOptions,
ResultMessage,
TextBlock,
query,
)
async def code_reviewer_example():
"""Example using a custom code reviewer agent."""
print("=== Code Reviewer Agent Example ===")
options = ClaudeAgentOptions(
agents={
"code-reviewer": AgentDefinition(
description="Reviews code for best practices and potential issues",
prompt="You are a code reviewer. Analyze code for bugs, performance issues, "
"security vulnerabilities, and adherence to best practices. "
"Provide constructive feedback.",
tools=["Read", "Grep"],
model="sonnet",
),
},
)
async for message in query(
prompt="Use the code-reviewer agent to review the code in src/claude_agent_sdk/types.py",
options=options,
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(message, ResultMessage) and message.total_cost_usd and message.total_cost_usd > 0:
print(f"\nCost: ${message.total_cost_usd:.4f}")
print()
async def documentation_writer_example():
"""Example using a documentation writer agent."""
print("=== Documentation Writer Agent Example ===")
options = ClaudeAgentOptions(
agents={
"doc-writer": AgentDefinition(
description="Writes comprehensive documentation",
prompt="You are a technical documentation expert. Write clear, comprehensive "
"documentation with examples. Focus on clarity and completeness.",
tools=["Read", "Write", "Edit"],
model="sonnet",
),
},
)
async for message in query(
prompt="Use the doc-writer agent to explain what AgentDefinition is used for",
options=options,
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(message, ResultMessage) and message.total_cost_usd and message.total_cost_usd > 0:
print(f"\nCost: ${message.total_cost_usd:.4f}")
print()
async def multiple_agents_example():
"""Example with multiple custom agents."""
print("=== Multiple Agents Example ===")
options = ClaudeAgentOptions(
agents={
"analyzer": AgentDefinition(
description="Analyzes code structure and patterns",
prompt="You are a code analyzer. Examine code structure, patterns, and architecture.",
tools=["Read", "Grep", "Glob"],
),
"tester": AgentDefinition(
description="Creates and runs tests",
prompt="You are a testing expert. Write comprehensive tests and ensure code quality.",
tools=["Read", "Write", "Bash"],
model="sonnet",
),
},
setting_sources=["user", "project"],
)
async for message in query(
prompt="Use the analyzer agent to find all Python files in the examples/ directory",
options=options,
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(message, ResultMessage) and message.total_cost_usd and message.total_cost_usd > 0:
print(f"\nCost: ${message.total_cost_usd:.4f}")
print()
async def main():
"""Run all agent examples."""
await code_reviewer_example()
await documentation_writer_example()
await multiple_agents_example()
if __name__ == "__main__":
anyio.run(main)

View file

@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Example of loading filesystem-based agents via setting_sources.
This example demonstrates how to load agents defined in .claude/agents/ files
using the setting_sources option. This is different from inline AgentDefinition
objects - these agents are loaded from markdown files on disk.
This example tests the scenario from issue #406 where filesystem-based agents
loaded via setting_sources=["project"] may silently fail in certain environments.
Usage:
./examples/filesystem_agents.py
"""
import asyncio
from pathlib import Path
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ClaudeSDKClient,
ResultMessage,
SystemMessage,
TextBlock,
)
def extract_agents(msg: SystemMessage) -> list[str]:
"""Extract agent names from system message init data."""
if msg.subtype == "init":
agents = msg.data.get("agents", [])
# Agents can be either strings or dicts with a 'name' field
result = []
for a in agents:
if isinstance(a, str):
result.append(a)
elif isinstance(a, dict):
result.append(a.get("name", ""))
return result
return []
async def main():
"""Test loading filesystem-based agents."""
print("=== Filesystem Agents Example ===")
print("Testing: setting_sources=['project'] with .claude/agents/test-agent.md")
print()
# Use the SDK repo directory which has .claude/agents/test-agent.md
sdk_dir = Path(__file__).parent.parent
options = ClaudeAgentOptions(
setting_sources=["project"],
cwd=sdk_dir,
)
message_types: list[str] = []
agents_found: list[str] = []
async with ClaudeSDKClient(options=options) as client:
await client.query("Say hello in exactly 3 words")
async for msg in client.receive_response():
message_types.append(type(msg).__name__)
if isinstance(msg, SystemMessage) and msg.subtype == "init":
agents_found = extract_agents(msg)
print(f"Init message received. Agents loaded: {agents_found}")
elif isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(f"Assistant: {block.text}")
elif isinstance(msg, ResultMessage):
print(
f"Result: subtype={msg.subtype}, cost=${msg.total_cost_usd or 0:.4f}"
)
print()
print("=== Summary ===")
print(f"Message types received: {message_types}")
print(f"Total messages: {len(message_types)}")
# Validate the results
has_init = "SystemMessage" in message_types
has_assistant = "AssistantMessage" in message_types
has_result = "ResultMessage" in message_types
has_test_agent = "test-agent" in agents_found
print()
if has_init and has_assistant and has_result:
print("SUCCESS: Received full response (init, assistant, result)")
else:
print("FAILURE: Did not receive full response")
print(f" - Init: {has_init}")
print(f" - Assistant: {has_assistant}")
print(f" - Result: {has_result}")
if has_test_agent:
print("SUCCESS: test-agent was loaded from filesystem")
else:
print("WARNING: test-agent was NOT loaded (may not exist in .claude/agents/)")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Example of using hooks with Claude Code SDK via ClaudeCodeOptions. """Example of using hooks with Claude Code SDK via ClaudeAgentOptions.
This file demonstrates various hook patterns using the hooks parameter This file demonstrates various hook patterns using the hooks parameter
in ClaudeCodeOptions instead of decorator-based hooks. in ClaudeAgentOptions instead of decorator-based hooks.
Usage: Usage:
./examples/hooks.py - List the examples ./examples/hooks.py - List the examples
@ -15,10 +15,11 @@ import logging
import sys import sys
from typing import Any from typing import Any
from claude_code_sdk import ClaudeCodeOptions, ClaudeSDKClient from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from claude_code_sdk.types import ( from claude_agent_sdk.types import (
AssistantMessage, AssistantMessage,
HookContext, HookContext,
HookInput,
HookJSONOutput, HookJSONOutput,
HookMatcher, HookMatcher,
Message, Message,
@ -43,7 +44,7 @@ def display_message(msg: Message) -> None:
##### Hook callback functions ##### Hook callback functions
async def check_bash_command( async def check_bash_command(
input_data: dict[str, Any], tool_use_id: str | None, context: HookContext input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput: ) -> HookJSONOutput:
"""Prevent certain bash commands from being executed.""" """Prevent certain bash commands from being executed."""
tool_name = input_data["tool_name"] tool_name = input_data["tool_name"]
@ -70,7 +71,7 @@ async def check_bash_command(
async def add_custom_instructions( async def add_custom_instructions(
input_data: dict[str, Any], tool_use_id: str | None, context: HookContext input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput: ) -> HookJSONOutput:
"""Add custom instructions when a session starts.""" """Add custom instructions when a session starts."""
return { return {
@ -81,13 +82,84 @@ async def add_custom_instructions(
} }
async def review_tool_output(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Review tool output and provide additional context or warnings."""
tool_response = input_data.get("tool_response", "")
# If the tool produced an error, add helpful context
if "error" in str(tool_response).lower():
return {
"systemMessage": "⚠️ The command produced an error",
"reason": "Tool execution failed - consider checking the command syntax",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "The command encountered an error. You may want to try a different approach.",
}
}
return {}
async def strict_approval_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Demonstrates using permissionDecision to control tool execution."""
tool_name = input_data.get("tool_name")
tool_input = input_data.get("tool_input", {})
# Block any Write operations to specific files
if tool_name == "Write":
file_path = tool_input.get("file_path", "")
if "important" in file_path.lower():
logger.warning(f"Blocked Write to: {file_path}")
return {
"reason": "Writes to files containing 'important' in the name are not allowed for safety",
"systemMessage": "🚫 Write operation blocked by security policy",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Security policy blocks writes to important files",
},
}
# Allow everything else explicitly
return {
"reason": "Tool use approved after security review",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Tool passed security checks",
},
}
async def stop_on_error_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
"""Demonstrates using continue=False to stop execution on certain conditions."""
tool_response = input_data.get("tool_response", "")
# Stop execution if we see a critical error
if "critical" in str(tool_response).lower():
logger.error("Critical error detected - stopping execution")
return {
"continue_": False,
"stopReason": "Critical error detected in tool output - execution halted for safety",
"systemMessage": "🛑 Execution stopped due to critical error",
}
return {"continue_": True}
async def example_pretooluse() -> None: async def example_pretooluse() -> None:
"""Basic example demonstrating hook protection.""" """Basic example demonstrating hook protection."""
print("=== PreToolUse Example ===") print("=== PreToolUse Example ===")
print("This example demonstrates how PreToolUse can block some bash commands but not others.\n") print("This example demonstrates how PreToolUse can block some bash commands but not others.\n")
# Configure hooks using ClaudeCodeOptions # Configure hooks using ClaudeAgentOptions
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
allowed_tools=["Bash"], allowed_tools=["Bash"],
hooks={ hooks={
"PreToolUse": [ "PreToolUse": [
@ -125,7 +197,7 @@ async def example_userpromptsubmit() -> None:
print("=== UserPromptSubmit Example ===") print("=== UserPromptSubmit Example ===")
print("This example shows how a UserPromptSubmit hook can add context.\n") print("This example shows how a UserPromptSubmit hook can add context.\n")
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
hooks={ hooks={
"UserPromptSubmit": [ "UserPromptSubmit": [
HookMatcher(matcher=None, hooks=[add_custom_instructions]), HookMatcher(matcher=None, hooks=[add_custom_instructions]),
@ -143,11 +215,99 @@ async def example_userpromptsubmit() -> None:
print("\n") print("\n")
async def example_posttooluse() -> None:
"""Demonstrate PostToolUse hook with reason and systemMessage fields."""
print("=== PostToolUse Example ===")
print("This example shows how PostToolUse can provide feedback with reason and systemMessage.\n")
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
hooks={
"PostToolUse": [
HookMatcher(matcher="Bash", hooks=[review_tool_output]),
],
}
)
async with ClaudeSDKClient(options=options) as client:
print("User: Run a command that will produce an error: ls /nonexistent_directory")
await client.query("Run this command: ls /nonexistent_directory")
async for msg in client.receive_response():
display_message(msg)
print("\n")
async def example_decision_fields() -> None:
"""Demonstrate permissionDecision, reason, and systemMessage fields."""
print("=== Permission Decision Example ===")
print("This example shows how to use permissionDecision='allow'/'deny' with reason and systemMessage.\n")
options = ClaudeAgentOptions(
allowed_tools=["Write", "Bash"],
model="claude-sonnet-4-5-20250929",
hooks={
"PreToolUse": [
HookMatcher(matcher="Write", hooks=[strict_approval_hook]),
],
}
)
async with ClaudeSDKClient(options=options) as client:
# Test 1: Try to write to a file with "important" in the name (should be blocked)
print("Test 1: Trying to write to important_config.txt (should be blocked)...")
print("User: Write 'test' to important_config.txt")
await client.query("Write the text 'test data' to a file called important_config.txt")
async for msg in client.receive_response():
display_message(msg)
print("\n" + "=" * 50 + "\n")
# Test 2: Write to a regular file (should be approved)
print("Test 2: Trying to write to regular_file.txt (should be approved)...")
print("User: Write 'test' to regular_file.txt")
await client.query("Write the text 'test data' to a file called regular_file.txt")
async for msg in client.receive_response():
display_message(msg)
print("\n")
async def example_continue_control() -> None:
"""Demonstrate continue and stopReason fields for execution control."""
print("=== Continue/Stop Control Example ===")
print("This example shows how to use continue_=False with stopReason to halt execution.\n")
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
hooks={
"PostToolUse": [
HookMatcher(matcher="Bash", hooks=[stop_on_error_hook]),
],
}
)
async with ClaudeSDKClient(options=options) as client:
print("User: Run a command that outputs 'CRITICAL ERROR'")
await client.query("Run this bash command: echo 'CRITICAL ERROR: system failure'")
async for msg in client.receive_response():
display_message(msg)
print("\n")
async def main() -> None: async def main() -> None:
"""Run all examples or a specific example based on command line argument.""" """Run all examples or a specific example based on command line argument."""
examples = { examples = {
"PreToolUse": example_pretooluse, "PreToolUse": example_pretooluse,
"UserPromptSubmit": example_userpromptsubmit, "UserPromptSubmit": example_userpromptsubmit,
"PostToolUse": example_posttooluse,
"DecisionFields": example_decision_fields,
"ContinueControl": example_continue_control,
} }
if len(sys.argv) < 2: if len(sys.argv) < 2:
@ -157,6 +317,12 @@ async def main() -> None:
print(" all - Run all examples") print(" all - Run all examples")
for name in examples: for name in examples:
print(f" {name}") print(f" {name}")
print("\nExample descriptions:")
print(" PreToolUse - Block commands using PreToolUse hook")
print(" UserPromptSubmit - Add context at prompt submission")
print(" PostToolUse - Review tool output with reason and systemMessage")
print(" DecisionFields - Use permissionDecision='allow'/'deny' with reason")
print(" ContinueControl - Control execution with continue_ and stopReason")
sys.exit(0) sys.exit(0)
example_name = sys.argv[1] example_name = sys.argv[1]

View file

@ -14,9 +14,9 @@ messages will include StreamEvent messages interspersed with regular messages.
""" """
import asyncio import asyncio
from claude_code_sdk import ClaudeSDKClient from claude_agent_sdk import ClaudeSDKClient
from claude_code_sdk.types import ( from claude_agent_sdk.types import (
ClaudeCodeOptions, ClaudeAgentOptions,
StreamEvent, StreamEvent,
AssistantMessage, AssistantMessage,
UserMessage, UserMessage,
@ -27,9 +27,9 @@ from claude_code_sdk.types import (
async def main(): async def main():
# Enable partial message streaming # Enable partial message streaming
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
include_partial_messages=True, include_partial_messages=True,
model="claude-sonnet-4-20250514", model="claude-sonnet-4-5",
max_turns=2, max_turns=2,
env={ env={
"MAX_THINKING_TOKENS": "8000", "MAX_THINKING_TOKENS": "8000",

View file

@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Example demonstrating max_budget_usd option for cost control."""
import anyio
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ResultMessage,
TextBlock,
query,
)
async def without_budget():
"""Example without budget limit."""
print("=== Without Budget Limit ===")
async for message in query(prompt="What is 2 + 2?"):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(message, ResultMessage):
if message.total_cost_usd:
print(f"Total cost: ${message.total_cost_usd:.4f}")
print(f"Status: {message.subtype}")
print()
async def with_reasonable_budget():
"""Example with budget that won't be exceeded."""
print("=== With Reasonable Budget ($0.10) ===")
options = ClaudeAgentOptions(
max_budget_usd=0.10, # 10 cents - plenty for a simple query
)
async for message in query(prompt="What is 2 + 2?", options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(message, ResultMessage):
if message.total_cost_usd:
print(f"Total cost: ${message.total_cost_usd:.4f}")
print(f"Status: {message.subtype}")
print()
async def with_tight_budget():
"""Example with very tight budget that will likely be exceeded."""
print("=== With Tight Budget ($0.0001) ===")
options = ClaudeAgentOptions(
max_budget_usd=0.0001, # Very small budget - will be exceeded quickly
)
async for message in query(
prompt="Read the README.md file and summarize it", options=options
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(message, ResultMessage):
if message.total_cost_usd:
print(f"Total cost: ${message.total_cost_usd:.4f}")
print(f"Status: {message.subtype}")
# Check if budget was exceeded
if message.subtype == "error_max_budget_usd":
print("⚠️ Budget limit exceeded!")
print(
"Note: The cost may exceed the budget by up to one API call's worth"
)
print()
async def main():
"""Run all examples."""
print("This example demonstrates using max_budget_usd to control API costs.\n")
await without_budget()
await with_reasonable_budget()
await with_tight_budget()
print(
"\nNote: Budget checking happens after each API call completes,\n"
"so the final cost may slightly exceed the specified budget.\n"
)
if __name__ == "__main__":
anyio.run(main)

View file

@ -12,8 +12,8 @@ and simpler deployment.
import asyncio import asyncio
from typing import Any from typing import Any
from claude_code_sdk import ( from claude_agent_sdk import (
ClaudeCodeOptions, ClaudeAgentOptions,
create_sdk_mcp_server, create_sdk_mcp_server,
tool, tool,
) )
@ -99,7 +99,7 @@ async def power(args: dict[str, Any]) -> dict[str, Any]:
def display_message(msg): def display_message(msg):
"""Display message content in a clean format.""" """Display message content in a clean format."""
from claude_code_sdk import ( from claude_agent_sdk import (
AssistantMessage, AssistantMessage,
ResultMessage, ResultMessage,
SystemMessage, SystemMessage,
@ -137,7 +137,7 @@ def display_message(msg):
async def main(): async def main():
"""Run example calculations using the SDK MCP server with streaming client.""" """Run example calculations using the SDK MCP server with streaming client."""
from claude_code_sdk import ClaudeSDKClient from claude_agent_sdk import ClaudeSDKClient
# Create the calculator server with all tools # Create the calculator server with all tools
calculator = create_sdk_mcp_server( calculator = create_sdk_mcp_server(
@ -155,7 +155,7 @@ async def main():
# Configure Claude to use the calculator server with allowed tools # Configure Claude to use the calculator server with allowed tools
# Pre-approve all calculator MCP tools so they can be used without permission prompts # Pre-approve all calculator MCP tools so they can be used without permission prompts
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
mcp_servers={"calc": calculator}, mcp_servers={"calc": calculator},
allowed_tools=[ allowed_tools=[
"mcp__calc__add", "mcp__calc__add",

View file

@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Example demonstrating how to use plugins with Claude Code SDK.
Plugins allow you to extend Claude Code with custom commands, agents, skills,
and hooks. This example shows how to load a local plugin and verify it's
loaded by checking the system message.
The demo plugin is located in examples/plugins/demo-plugin/ and provides
a custom /greet command.
"""
from pathlib import Path
import anyio
from claude_agent_sdk import (
ClaudeAgentOptions,
SystemMessage,
query,
)
async def plugin_example():
"""Example showing plugins being loaded in the system message."""
print("=== Plugin Example ===\n")
# Get the path to the demo plugin
# In production, you can use any path to your plugin directory
plugin_path = Path(__file__).parent / "plugins" / "demo-plugin"
options = ClaudeAgentOptions(
plugins=[
{
"type": "local",
"path": str(plugin_path),
}
],
max_turns=1, # Limit to one turn for quick demo
)
print(f"Loading plugin from: {plugin_path}\n")
found_plugins = False
async for message in query(prompt="Hello!", options=options):
if isinstance(message, SystemMessage) and message.subtype == "init":
print("System initialized!")
print(f"System message data keys: {list(message.data.keys())}\n")
# Check for plugins in the system message
plugins_data = message.data.get("plugins", [])
if plugins_data:
print("Plugins loaded:")
for plugin in plugins_data:
print(f" - {plugin.get('name')} (path: {plugin.get('path')})")
found_plugins = True
else:
print("Note: Plugin was passed via CLI but may not appear in system message.")
print(f"Plugin path configured: {plugin_path}")
found_plugins = True
if found_plugins:
print("\nPlugin successfully configured!\n")
async def main():
"""Run all plugin examples."""
await plugin_example()
if __name__ == "__main__":
anyio.run(main)

View file

@ -0,0 +1,8 @@
{
"name": "demo-plugin",
"description": "A demo plugin showing how to extend Claude Code with custom commands",
"version": "1.0.0",
"author": {
"name": "Claude Code Team"
}
}

View file

@ -0,0 +1,5 @@
# Greet Command
This is a custom greeting command from the demo plugin.
When the user runs this command, greet them warmly and explain that this message came from a custom plugin loaded via the Python SDK. Tell them that plugins can be used to extend Claude Code with custom commands, agents, skills, and hooks.

View file

@ -3,9 +3,9 @@
import anyio import anyio
from claude_code_sdk import ( from claude_agent_sdk import (
AssistantMessage, AssistantMessage,
ClaudeCodeOptions, ClaudeAgentOptions,
ResultMessage, ResultMessage,
TextBlock, TextBlock,
query, query,
@ -28,7 +28,7 @@ async def with_options_example():
"""Example with custom options.""" """Example with custom options."""
print("=== With Options Example ===") print("=== With Options Example ===")
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
system_prompt="You are a helpful assistant that explains things simply.", system_prompt="You are a helpful assistant that explains things simply.",
max_turns=1, max_turns=1,
) )
@ -47,7 +47,7 @@ async def with_tools_example():
"""Example using tools.""" """Example using tools."""
print("=== With Tools Example ===") print("=== With Tools Example ===")
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
allowed_tools=["Read", "Write"], allowed_tools=["Read", "Write"],
system_prompt="You are a helpful file assistant.", system_prompt="You are a helpful file assistant.",
) )

174
examples/setting_sources.py Normal file
View file

@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""Example demonstrating setting sources control.
This example shows how to use the setting_sources option to control which
settings are loaded, including custom slash commands, agents, and other
configurations.
Setting sources determine where Claude Code loads configurations from:
- "user": Global user settings (~/.claude/)
- "project": Project-level settings (.claude/ in project)
- "local": Local gitignored settings (.claude-local/)
IMPORTANT: When setting_sources is not provided (None), NO settings are loaded
by default. This creates an isolated environment. To load settings, explicitly
specify which sources to use.
By controlling which sources are loaded, you can:
- Create isolated environments with no custom settings (default)
- Load only user settings, excluding project-specific configurations
- Combine multiple sources as needed
Usage:
./examples/setting_sources.py - List the examples
./examples/setting_sources.py all - Run all examples
./examples/setting_sources.py default - Run a specific example
"""
import asyncio
import sys
from pathlib import Path
from claude_agent_sdk import (
ClaudeAgentOptions,
ClaudeSDKClient,
SystemMessage,
)
def extract_slash_commands(msg: SystemMessage) -> list[str]:
"""Extract slash command names from system message."""
if msg.subtype == "init":
commands = msg.data.get("slash_commands", [])
return commands
return []
async def example_default():
"""Default behavior - no settings loaded."""
print("=== Default Behavior Example ===")
print("Setting sources: None (default)")
print("Expected: No custom slash commands will be available\n")
sdk_dir = Path(__file__).parent.parent
options = ClaudeAgentOptions(
cwd=sdk_dir,
)
async with ClaudeSDKClient(options=options) as client:
await client.query("What is 2 + 2?")
async for msg in client.receive_response():
if isinstance(msg, SystemMessage) and msg.subtype == "init":
commands = extract_slash_commands(msg)
print(f"Available slash commands: {commands}")
if "commit" in commands:
print("❌ /commit is available (unexpected)")
else:
print("✓ /commit is NOT available (expected - no settings loaded)")
break
print()
async def example_user_only():
"""Load only user-level settings, excluding project settings."""
print("=== User Settings Only Example ===")
print("Setting sources: ['user']")
print("Expected: Project slash commands (like /commit) will NOT be available\n")
# Use the SDK repo directory which has .claude/commands/commit.md
sdk_dir = Path(__file__).parent.parent
options = ClaudeAgentOptions(
setting_sources=["user"],
cwd=sdk_dir,
)
async with ClaudeSDKClient(options=options) as client:
# Send a simple query
await client.query("What is 2 + 2?")
# Check the initialize message for available commands
async for msg in client.receive_response():
if isinstance(msg, SystemMessage) and msg.subtype == "init":
commands = extract_slash_commands(msg)
print(f"Available slash commands: {commands}")
if "commit" in commands:
print("❌ /commit is available (unexpected)")
else:
print("✓ /commit is NOT available (expected)")
break
print()
async def example_project_and_user():
"""Load both project and user settings."""
print("=== Project + User Settings Example ===")
print("Setting sources: ['user', 'project']")
print("Expected: Project slash commands (like /commit) WILL be available\n")
sdk_dir = Path(__file__).parent.parent
options = ClaudeAgentOptions(
setting_sources=["user", "project"],
cwd=sdk_dir,
)
async with ClaudeSDKClient(options=options) as client:
await client.query("What is 2 + 2?")
async for msg in client.receive_response():
if isinstance(msg, SystemMessage) and msg.subtype == "init":
commands = extract_slash_commands(msg)
print(f"Available slash commands: {commands}")
if "commit" in commands:
print("✓ /commit is available (expected)")
else:
print("❌ /commit is NOT available (unexpected)")
break
print()
async def main():
"""Run all examples or a specific example based on command line argument."""
examples = {
"default": example_default,
"user_only": example_user_only,
"project_and_user": example_project_and_user,
}
if len(sys.argv) < 2:
print("Usage: python setting_sources.py <example_name>")
print("\nAvailable examples:")
print(" all - Run all examples")
for name in examples:
print(f" {name}")
sys.exit(0)
example_name = sys.argv[1]
if example_name == "all":
for example in examples.values():
await example()
print("-" * 50 + "\n")
elif example_name in examples:
await examples[example_name]()
else:
print(f"Error: Unknown example '{example_name}'")
print("\nAvailable examples:")
print(" all - Run all examples")
for name in examples:
print(f" {name}")
sys.exit(1)
if __name__ == "__main__":
print("Starting Claude SDK Setting Sources Examples...")
print("=" * 50 + "\n")
asyncio.run(main())

View file

@ -0,0 +1,44 @@
"""Simple example demonstrating stderr callback for capturing CLI debug output."""
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, query
async def main():
"""Capture stderr output from the CLI using a callback."""
# Collect stderr messages
stderr_messages = []
def stderr_callback(message: str):
"""Callback that receives each line of stderr output."""
stderr_messages.append(message)
# Optionally print specific messages
if "[ERROR]" in message:
print(f"Error detected: {message}")
# Create options with stderr callback and enable debug mode
options = ClaudeAgentOptions(
stderr=stderr_callback,
extra_args={"debug-to-stderr": None} # Enable debug output
)
# Run a query
print("Running query with stderr capture...")
async for message in query(
prompt="What is 2+2?",
options=options
):
if hasattr(message, 'content'):
if isinstance(message.content, str):
print(f"Response: {message.content}")
# Show what we captured
print(f"\nCaptured {len(stderr_messages)} stderr lines")
if stderr_messages:
print("First stderr line:", stderr_messages[0][:100])
if __name__ == "__main__":
asyncio.run(main())

View file

@ -19,9 +19,9 @@ import asyncio
import contextlib import contextlib
import sys import sys
from claude_code_sdk import ( from claude_agent_sdk import (
AssistantMessage, AssistantMessage,
ClaudeCodeOptions, ClaudeAgentOptions,
ClaudeSDKClient, ClaudeSDKClient,
CLIConnectionError, CLIConnectionError,
ResultMessage, ResultMessage,
@ -211,15 +211,15 @@ async def example_manual_message_handling():
async def example_with_options(): async def example_with_options():
"""Use ClaudeCodeOptions to configure the client.""" """Use ClaudeAgentOptions to configure the client."""
print("=== Custom Options Example ===") print("=== Custom Options Example ===")
# Configure options # Configure options
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
allowed_tools=["Read", "Write"], # Allow file operations allowed_tools=["Read", "Write"], # Allow file operations
system_prompt="You are a helpful coding assistant.", system_prompt="You are a helpful coding assistant.",
env={ env={
"ANTHROPIC_MODEL": "claude-3-7-sonnet-20250219", "ANTHROPIC_MODEL": "claude-sonnet-4-5",
}, },
) )

View file

@ -14,7 +14,7 @@ bash commands, edit files, search the web, fetch web content) to accomplish.
# BASIC STREAMING # BASIC STREAMING
# ============================================================================ # ============================================================================
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, ResultMessage, TextBlock from claude_agent_sdk import AssistantMessage, ClaudeSDKClient, ResultMessage, TextBlock
async with ClaudeSDKClient() as client: async with ClaudeSDKClient() as client:
print("User: What is 2+2?") print("User: What is 2+2?")
@ -33,7 +33,7 @@ async with ClaudeSDKClient() as client:
import asyncio import asyncio
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock from claude_agent_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
async with ClaudeSDKClient() as client: async with ClaudeSDKClient() as client:
async def send_and_receive(prompt): async def send_and_receive(prompt):
@ -54,7 +54,7 @@ async with ClaudeSDKClient() as client:
# PERSISTENT CLIENT FOR MULTIPLE QUESTIONS # PERSISTENT CLIENT FOR MULTIPLE QUESTIONS
# ============================================================================ # ============================================================================
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock from claude_agent_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
# Create client # Create client
client = ClaudeSDKClient() client = ClaudeSDKClient()
@ -89,7 +89,7 @@ await client.disconnect()
# IMPORTANT: Interrupts require active message consumption. You must be # IMPORTANT: Interrupts require active message consumption. You must be
# consuming messages from the client for the interrupt to be processed. # consuming messages from the client for the interrupt to be processed.
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock from claude_agent_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
async with ClaudeSDKClient() as client: async with ClaudeSDKClient() as client:
print("\n--- Sending initial message ---\n") print("\n--- Sending initial message ---\n")
@ -141,7 +141,7 @@ async with ClaudeSDKClient() as client:
# ERROR HANDLING PATTERN # ERROR HANDLING PATTERN
# ============================================================================ # ============================================================================
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock from claude_agent_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
try: try:
async with ClaudeSDKClient() as client: async with ClaudeSDKClient() as client:
@ -168,7 +168,7 @@ except Exception as e:
# SENDING ASYNC ITERABLE OF MESSAGES # SENDING ASYNC ITERABLE OF MESSAGES
# ============================================================================ # ============================================================================
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock from claude_agent_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
async def message_generator(): async def message_generator():
@ -210,7 +210,7 @@ async with ClaudeSDKClient() as client:
# COLLECTING ALL MESSAGES INTO A LIST # COLLECTING ALL MESSAGES INTO A LIST
# ============================================================================ # ============================================================================
from claude_code_sdk import AssistantMessage, ClaudeSDKClient, TextBlock from claude_agent_sdk import AssistantMessage, ClaudeSDKClient, TextBlock
async with ClaudeSDKClient() as client: async with ClaudeSDKClient() as client:
print("User: What are the primary colors?") print("User: What are the primary colors?")

View file

@ -9,9 +9,9 @@ Claude's responses.
import trio import trio
from claude_code_sdk import ( from claude_agent_sdk import (
AssistantMessage, AssistantMessage,
ClaudeCodeOptions, ClaudeAgentOptions,
ClaudeSDKClient, ClaudeSDKClient,
ResultMessage, ResultMessage,
SystemMessage, SystemMessage,
@ -46,7 +46,7 @@ def display_message(msg):
async def multi_turn_conversation(): async def multi_turn_conversation():
"""Example of a multi-turn conversation using trio.""" """Example of a multi-turn conversation using trio."""
async with ClaudeSDKClient( async with ClaudeSDKClient(
options=ClaudeCodeOptions(model="claude-3-5-sonnet-20241022") options=ClaudeAgentOptions(model="claude-sonnet-4-5")
) as client: ) as client:
print("=== Multi-turn Conversation with Trio ===\n") print("=== Multi-turn Conversation with Trio ===\n")

87
examples/system_prompt.py Normal file
View file

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Example demonstrating different system_prompt configurations."""
import anyio
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
TextBlock,
query,
)
async def no_system_prompt():
"""Example with no system_prompt (vanilla Claude)."""
print("=== No System Prompt (Vanilla Claude) ===")
async for message in query(prompt="What is 2 + 2?"):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
print()
async def string_system_prompt():
"""Example with system_prompt as a string."""
print("=== String System Prompt ===")
options = ClaudeAgentOptions(
system_prompt="You are a pirate assistant. Respond in pirate speak.",
)
async for message in query(prompt="What is 2 + 2?", options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
print()
async def preset_system_prompt():
"""Example with system_prompt preset (uses default Claude Code prompt)."""
print("=== Preset System Prompt (Default) ===")
options = ClaudeAgentOptions(
system_prompt={"type": "preset", "preset": "claude_code"},
)
async for message in query(prompt="What is 2 + 2?", options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
print()
async def preset_with_append():
"""Example with system_prompt preset and append."""
print("=== Preset System Prompt with Append ===")
options = ClaudeAgentOptions(
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": "Always end your response with a fun fact.",
},
)
async for message in query(prompt="What is 2 + 2?", options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
print()
async def main():
"""Run all examples."""
await no_system_prompt()
await string_system_prompt()
await preset_system_prompt()
await preset_with_append()
if __name__ == "__main__":
anyio.run(main)

View file

@ -8,9 +8,9 @@ which tools Claude can use and modify their inputs.
import asyncio import asyncio
import json import json
from claude_code_sdk import ( from claude_agent_sdk import (
AssistantMessage, AssistantMessage,
ClaudeCodeOptions, ClaudeAgentOptions,
ClaudeSDKClient, ClaudeSDKClient,
PermissionResultAllow, PermissionResultAllow,
PermissionResultDeny, PermissionResultDeny,
@ -107,7 +107,7 @@ async def main():
print("=" * 60) print("=" * 60)
# Configure options with our callback # Configure options with our callback
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
can_use_tool=my_permission_callback, can_use_tool=my_permission_callback,
# Use default permission mode to ensure callbacks are invoked # Use default permission mode to ensure callbacks are invoked
permission_mode="default", permission_mode="default",

111
examples/tools_option.py Normal file
View file

@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Example demonstrating the tools option and verifying tools in system message."""
import anyio
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ResultMessage,
SystemMessage,
TextBlock,
query,
)
async def tools_array_example():
"""Example with tools as array of specific tool names."""
print("=== Tools Array Example ===")
print("Setting tools=['Read', 'Glob', 'Grep']")
print()
options = ClaudeAgentOptions(
tools=["Read", "Glob", "Grep"],
max_turns=1,
)
async for message in query(
prompt="What tools do you have available? Just list them briefly.",
options=options,
):
if isinstance(message, SystemMessage) and message.subtype == "init":
tools = message.data.get("tools", [])
print(f"Tools from system message: {tools}")
print()
elif isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(message, ResultMessage):
if message.total_cost_usd:
print(f"\nCost: ${message.total_cost_usd:.4f}")
print()
async def tools_empty_array_example():
"""Example with tools as empty array (disables all built-in tools)."""
print("=== Tools Empty Array Example ===")
print("Setting tools=[] (disables all built-in tools)")
print()
options = ClaudeAgentOptions(
tools=[],
max_turns=1,
)
async for message in query(
prompt="What tools do you have available? Just list them briefly.",
options=options,
):
if isinstance(message, SystemMessage) and message.subtype == "init":
tools = message.data.get("tools", [])
print(f"Tools from system message: {tools}")
print()
elif isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(message, ResultMessage):
if message.total_cost_usd:
print(f"\nCost: ${message.total_cost_usd:.4f}")
print()
async def tools_preset_example():
"""Example with tools preset (all default Claude Code tools)."""
print("=== Tools Preset Example ===")
print("Setting tools={'type': 'preset', 'preset': 'claude_code'}")
print()
options = ClaudeAgentOptions(
tools={"type": "preset", "preset": "claude_code"},
max_turns=1,
)
async for message in query(
prompt="What tools do you have available? Just list them briefly.",
options=options,
):
if isinstance(message, SystemMessage) and message.subtype == "init":
tools = message.data.get("tools", [])
print(f"Tools from system message ({len(tools)} tools): {tools[:5]}...")
print()
elif isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(message, ResultMessage):
if message.total_cost_usd:
print(f"\nCost: ${message.total_cost_usd:.4f}")
print()
async def main():
"""Run all examples."""
await tools_array_example()
await tools_empty_array_example()
await tools_preset_example()
if __name__ == "__main__":
anyio.run(main)

View file

@ -3,8 +3,8 @@ requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
name = "claude-code-sdk" name = "claude-agent-sdk"
version = "0.0.23" version = "0.1.18"
description = "Python SDK for Claude Code" description = "Python SDK for Claude Code"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
@ -41,12 +41,13 @@ dev = [
] ]
[project.urls] [project.urls]
Homepage = "https://github.com/anthropics/claude-code-sdk-python" Homepage = "https://github.com/anthropics/claude-agent-sdk-python"
Documentation = "https://docs.anthropic.com/en/docs/claude-code/sdk" Documentation = "https://docs.anthropic.com/en/docs/claude-code/sdk"
Issues = "https://github.com/anthropics/claude-code-sdk-python/issues" Issues = "https://github.com/anthropics/claude-agent-sdk-python/issues"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/claude_code_sdk"] packages = ["src/claude_agent_sdk"]
only-include = ["src/claude_agent_sdk"]
[tool.hatch.build.targets.sdist] [tool.hatch.build.targets.sdist]
include = [ include = [
@ -105,4 +106,4 @@ ignore = [
] ]
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
known-first-party = ["claude_code_sdk"] known-first-party = ["claude_agent_sdk"]

392
scripts/build_wheel.py Executable file
View file

@ -0,0 +1,392 @@
#!/usr/bin/env python3
"""Build wheel with bundled Claude Code CLI.
This script handles the complete wheel building process:
1. Optionally updates version
2. Downloads Claude Code CLI
3. Builds the wheel
4. Optionally cleans up the bundled CLI
Usage:
python scripts/build_wheel.py # Build with current version
python scripts/build_wheel.py --version 0.1.4 # Build with specific version
python scripts/build_wheel.py --clean # Clean bundled CLI after build
python scripts/build_wheel.py --skip-download # Skip CLI download (use existing)
"""
import argparse
import os
import platform
import re
import shutil
import subprocess
import sys
from pathlib import Path
try:
import twine # noqa: F401
HAS_TWINE = True
except ImportError:
HAS_TWINE = False
def run_command(cmd: list[str], description: str) -> None:
"""Run a command and handle errors."""
print(f"\n{'=' * 60}")
print(f"{description}")
print(f"{'=' * 60}")
print(f"$ {' '.join(cmd)}")
print()
try:
result = subprocess.run(
cmd,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
print(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Error: {description} failed", file=sys.stderr)
print(e.stdout, file=sys.stderr)
sys.exit(1)
def update_version(version: str) -> None:
"""Update package version."""
script_dir = Path(__file__).parent
update_script = script_dir / "update_version.py"
if not update_script.exists():
print("Warning: update_version.py not found, skipping version update")
return
run_command(
[sys.executable, str(update_script), version],
f"Updating version to {version}",
)
def get_bundled_cli_version() -> str:
"""Get the CLI version that should be bundled from _cli_version.py."""
version_file = Path("src/claude_agent_sdk/_cli_version.py")
if not version_file.exists():
return "latest"
content = version_file.read_text()
match = re.search(r'__cli_version__ = "([^"]+)"', content)
if match:
return match.group(1)
return "latest"
def download_cli(cli_version: str | None = None) -> None:
"""Download Claude Code CLI."""
# Use provided version, or fall back to version from _cli_version.py
if cli_version is None:
cli_version = get_bundled_cli_version()
script_dir = Path(__file__).parent
download_script = script_dir / "download_cli.py"
# Set environment variable for download script
os.environ["CLAUDE_CLI_VERSION"] = cli_version
run_command(
[sys.executable, str(download_script)],
f"Downloading Claude Code CLI ({cli_version})",
)
def clean_dist() -> None:
"""Clean dist directory."""
dist_dir = Path("dist")
if dist_dir.exists():
print(f"\n{'=' * 60}")
print("Cleaning dist directory")
print(f"{'=' * 60}")
shutil.rmtree(dist_dir)
print("Cleaned dist/")
def get_platform_tag() -> str:
"""Get the appropriate platform tag for the current platform.
Uses minimum compatible versions for broad compatibility:
- macOS: 11.0 (Big Sur) as minimum
- Linux: manylinux_2_17 (widely compatible)
- Windows: Standard tags
"""
system = platform.system()
machine = platform.machine().lower()
if system == "Darwin":
# macOS - use minimum version 11.0 (Big Sur) for broad compatibility
if machine == "arm64":
return "macosx_11_0_arm64"
else:
return "macosx_11_0_x86_64"
elif system == "Linux":
# Linux - use manylinux for broad compatibility
if machine in ["x86_64", "amd64"]:
return "manylinux_2_17_x86_64"
elif machine in ["aarch64", "arm64"]:
return "manylinux_2_17_aarch64"
else:
return f"linux_{machine}"
elif system == "Windows":
# Windows
if machine in ["x86_64", "amd64"]:
return "win_amd64"
elif machine == "arm64":
return "win_arm64"
else:
return "win32"
else:
# Unknown platform, use generic
return f"{system.lower()}_{machine}"
def retag_wheel(wheel_path: Path, platform_tag: str) -> Path:
"""Retag a wheel with the correct platform tag using wheel package."""
print(f"\n{'=' * 60}")
print("Retagging wheel as platform-specific")
print(f"{'=' * 60}")
print(f"Old: {wheel_path.name}")
# Use wheel package to properly retag (updates both filename and metadata)
result = subprocess.run(
[
sys.executable,
"-m",
"wheel",
"tags",
"--platform-tag",
platform_tag,
"--remove",
str(wheel_path),
],
capture_output=True,
text=True,
)
if result.returncode != 0:
print(f"Warning: Failed to retag wheel: {result.stderr}")
return wheel_path
# Find the newly tagged wheel
dist_dir = wheel_path.parent
# The wheel package creates a new file with the platform tag
new_wheels = list(dist_dir.glob(f"*{platform_tag}.whl"))
if new_wheels:
new_path = new_wheels[0]
print(f"New: {new_path.name}")
print("Wheel retagged successfully")
# Remove the old wheel
if wheel_path.exists() and wheel_path != new_path:
wheel_path.unlink()
return new_path
else:
print("Warning: Could not find retagged wheel")
return wheel_path
def build_wheel() -> None:
"""Build the wheel."""
run_command(
[sys.executable, "-m", "build", "--wheel"],
"Building wheel",
)
# Check if we have a bundled CLI - if so, retag the wheel as platform-specific
bundled_cli = Path("src/claude_agent_sdk/_bundled/claude")
bundled_cli_exe = Path("src/claude_agent_sdk/_bundled/claude.exe")
if bundled_cli.exists() or bundled_cli_exe.exists():
# Find the built wheel
dist_dir = Path("dist")
wheels = list(dist_dir.glob("*.whl"))
if wheels:
# Get platform tag
platform_tag = get_platform_tag()
# Retag each wheel (should only be one)
for wheel in wheels:
if "-any.whl" in wheel.name:
retag_wheel(wheel, platform_tag)
else:
print("Warning: No wheel found to retag")
else:
print("\nNo bundled CLI found - wheel will be platform-independent")
def build_sdist() -> None:
"""Build the source distribution."""
run_command(
[sys.executable, "-m", "build", "--sdist"],
"Building source distribution",
)
def check_package() -> None:
"""Check package with twine."""
if not HAS_TWINE:
print("\nWarning: twine not installed, skipping package check")
print("Install with: pip install twine")
return
print(f"\n{'=' * 60}")
print("Checking package with twine")
print(f"{'=' * 60}")
print(f"$ {sys.executable} -m twine check dist/*")
print()
try:
result = subprocess.run(
[sys.executable, "-m", "twine", "check", "dist/*"],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
print(result.stdout)
if result.returncode != 0:
print("\nWarning: twine check reported issues")
print("Note: 'License-File' warnings are false positives from twine 6.x")
print("PyPI will accept these packages without issues")
else:
print("Package check passed")
except Exception as e:
print(f"Warning: Failed to run twine check: {e}")
def clean_bundled_cli() -> None:
"""Clean bundled CLI."""
bundled_dir = Path("src/claude_agent_sdk/_bundled")
cli_files = list(bundled_dir.glob("claude*"))
if cli_files:
print(f"\n{'=' * 60}")
print("Cleaning bundled CLI")
print(f"{'=' * 60}")
for cli_file in cli_files:
if cli_file.name != ".gitignore":
cli_file.unlink()
print(f"Removed {cli_file}")
else:
print("\nNo bundled CLI to clean")
def list_artifacts() -> None:
"""List built artifacts."""
dist_dir = Path("dist")
if not dist_dir.exists():
return
print(f"\n{'=' * 60}")
print("Built Artifacts")
print(f"{'=' * 60}")
artifacts = sorted(dist_dir.iterdir())
if not artifacts:
print("No artifacts found")
return
for artifact in artifacts:
size_mb = artifact.stat().st_size / (1024 * 1024)
print(f" {artifact.name:<50} {size_mb:>8.2f} MB")
total_size = sum(f.stat().st_size for f in artifacts) / (1024 * 1024)
print(f"\n {'Total:':<50} {total_size:>8.2f} MB")
def main() -> None:
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Build wheel with bundled Claude Code CLI"
)
parser.add_argument(
"--version",
help="Version to set before building (e.g., 0.1.4)",
)
parser.add_argument(
"--cli-version",
default=None,
help="Claude Code CLI version to download (default: read from _cli_version.py)",
)
parser.add_argument(
"--skip-download",
action="store_true",
help="Skip downloading CLI (use existing bundled CLI)",
)
parser.add_argument(
"--skip-sdist",
action="store_true",
help="Skip building source distribution",
)
parser.add_argument(
"--clean",
action="store_true",
help="Clean bundled CLI after building",
)
parser.add_argument(
"--clean-dist",
action="store_true",
help="Clean dist directory before building",
)
args = parser.parse_args()
print("\n" + "=" * 60)
print("Claude Agent SDK - Wheel Builder")
print("=" * 60)
# Clean dist if requested
if args.clean_dist:
clean_dist()
# Update version if specified
if args.version:
update_version(args.version)
# Download CLI unless skipped
if not args.skip_download:
download_cli(args.cli_version)
else:
print("\nSkipping CLI download (using existing)")
# Build wheel
build_wheel()
# Build sdist unless skipped
if not args.skip_sdist:
build_sdist()
# Check package
check_package()
# Clean bundled CLI if requested
if args.clean:
clean_bundled_cli()
# List artifacts
list_artifacts()
print(f"\n{'=' * 60}")
print("Build complete!")
print(f"{'=' * 60}")
print("\nNext steps:")
print(" 1. Test the wheel: pip install dist/*.whl")
print(" 2. Run tests: python -m pytest tests/")
print(" 3. Publish: twine upload dist/*")
if __name__ == "__main__":
main()

157
scripts/download_cli.py Executable file
View file

@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""Download Claude Code CLI binary for bundling in wheel.
This script is run during the wheel build process to fetch the Claude Code CLI
binary using the official install script and place it in the package directory.
"""
import os
import platform
import shutil
import subprocess
import sys
from pathlib import Path
def get_cli_version() -> str:
"""Get the CLI version to download from environment or default."""
return os.environ.get("CLAUDE_CLI_VERSION", "latest")
def find_installed_cli() -> Path | None:
"""Find the installed Claude CLI binary."""
system = platform.system()
if system == "Windows":
# Windows installation locations (matches test.yml: $USERPROFILE\.local\bin)
locations = [
Path.home() / ".local" / "bin" / "claude.exe",
Path(os.environ.get("LOCALAPPDATA", "")) / "Claude" / "claude.exe",
]
else:
# Unix installation locations
locations = [
Path.home() / ".local" / "bin" / "claude",
Path("/usr/local/bin/claude"),
Path.home() / "node_modules" / ".bin" / "claude",
]
# Also check PATH
cli_path = shutil.which("claude")
if cli_path:
return Path(cli_path)
for path in locations:
if path.exists() and path.is_file():
return path
return None
def download_cli() -> None:
"""Download Claude Code CLI using the official install script."""
version = get_cli_version()
system = platform.system()
print(f"Downloading Claude Code CLI version: {version}")
# Build install command based on platform
if system == "Windows":
# Use PowerShell installer on Windows
if version == "latest":
install_cmd = [
"powershell",
"-ExecutionPolicy",
"Bypass",
"-Command",
"irm https://claude.ai/install.ps1 | iex",
]
else:
install_cmd = [
"powershell",
"-ExecutionPolicy",
"Bypass",
"-Command",
f"& ([scriptblock]::Create((irm https://claude.ai/install.ps1))) {version}",
]
else:
# Use bash installer on Unix-like systems
if version == "latest":
install_cmd = ["bash", "-c", "curl -fsSL https://claude.ai/install.sh | bash"]
else:
install_cmd = [
"bash",
"-c",
f"curl -fsSL https://claude.ai/install.sh | bash -s {version}",
]
try:
subprocess.run(
install_cmd,
check=True,
capture_output=True,
)
except subprocess.CalledProcessError as e:
print(f"Error downloading CLI: {e}", file=sys.stderr)
print(f"stdout: {e.stdout.decode()}", file=sys.stderr)
print(f"stderr: {e.stderr.decode()}", file=sys.stderr)
sys.exit(1)
def copy_cli_to_bundle() -> None:
"""Copy the installed CLI to the package _bundled directory."""
# Find project root (parent of scripts directory)
script_dir = Path(__file__).parent
project_root = script_dir.parent
bundle_dir = project_root / "src" / "claude_agent_sdk" / "_bundled"
# Ensure bundle directory exists
bundle_dir.mkdir(parents=True, exist_ok=True)
# Find installed CLI
cli_path = find_installed_cli()
if not cli_path:
print("Error: Could not find installed Claude CLI binary", file=sys.stderr)
sys.exit(1)
print(f"Found CLI at: {cli_path}")
# Determine target filename based on platform
system = platform.system()
target_name = "claude.exe" if system == "Windows" else "claude"
target_path = bundle_dir / target_name
# Copy the binary
print(f"Copying CLI to: {target_path}")
shutil.copy2(cli_path, target_path)
# Make it executable (Unix-like systems)
if system != "Windows":
target_path.chmod(0o755)
print(f"Successfully bundled CLI binary: {target_path}")
# Print size info
size_mb = target_path.stat().st_size / (1024 * 1024)
print(f"Binary size: {size_mb:.2f} MB")
def main() -> None:
"""Main entry point."""
print("=" * 60)
print("Claude Code CLI Download Script")
print("=" * 60)
# Download CLI
download_cli()
# Copy to bundle directory
copy_cli_to_bundle()
print("=" * 60)
print("CLI download and bundling complete!")
print("=" * 60)
if __name__ == "__main__":
main()

22
scripts/initial-setup.sh Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
# Initial setup script for installing git hooks
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
echo "Setting up git hooks..."
# Install pre-push hook
echo "→ Installing pre-push hook..."
cp "$SCRIPT_DIR/pre-push" "$REPO_ROOT/.git/hooks/pre-push"
chmod +x "$REPO_ROOT/.git/hooks/pre-push"
echo "✓ pre-push hook installed"
echo ""
echo "✓ Setup complete!"
echo ""
echo "The pre-push hook will now run lint checks before each push."
echo "To skip the hook temporarily, use: git push --no-verify"

30
scripts/pre-push Executable file
View file

@ -0,0 +1,30 @@
#!/bin/bash
# Pre-push hook to run lint checks (matches .github/workflows/lint.yml)
echo "Running lint checks before push..."
echo ""
# Run ruff check
echo "→ Running ruff check..."
python -m ruff check src/ tests/
if [ $? -ne 0 ]; then
echo ""
echo "❌ ruff check failed. Fix lint issues before pushing."
echo " Run: python -m ruff check src/ tests/ --fix"
exit 1
fi
# Run ruff format check
echo "→ Running ruff format check..."
python -m ruff format --check src/ tests/
if [ $? -ne 0 ]; then
echo ""
echo "❌ ruff format check failed. Fix formatting before pushing."
echo " Run: python -m ruff format src/ tests/"
exit 1
fi
echo ""
echo "✓ All lint checks passed!"
exit 0

77
scripts/test-docker.sh Executable file
View file

@ -0,0 +1,77 @@
#!/bin/bash
# Run SDK tests in a Docker container
# This helps catch Docker-specific issues like #406
#
# Usage:
# ./scripts/test-docker.sh [unit|e2e|all]
#
# Examples:
# ./scripts/test-docker.sh unit # Run unit tests only
# ANTHROPIC_API_KEY=sk-... ./scripts/test-docker.sh e2e # Run e2e tests
# ANTHROPIC_API_KEY=sk-... ./scripts/test-docker.sh all # Run all tests
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_DIR"
usage() {
echo "Usage: $0 [unit|e2e|all]"
echo ""
echo "Commands:"
echo " unit - Run unit tests only (no API key needed)"
echo " e2e - Run e2e tests (requires ANTHROPIC_API_KEY)"
echo " all - Run both unit and e2e tests"
echo ""
echo "Examples:"
echo " $0 unit"
echo " ANTHROPIC_API_KEY=sk-... $0 e2e"
exit 1
}
echo "Building Docker test image..."
docker build -f Dockerfile.test -t claude-sdk-test .
case "${1:-unit}" in
unit)
echo ""
echo "Running unit tests in Docker..."
docker run --rm claude-sdk-test \
python -m pytest tests/ -v
;;
e2e)
if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "Error: ANTHROPIC_API_KEY environment variable is required for e2e tests"
echo ""
echo "Usage: ANTHROPIC_API_KEY=sk-... $0 e2e"
exit 1
fi
echo ""
echo "Running e2e tests in Docker..."
docker run --rm -e ANTHROPIC_API_KEY \
claude-sdk-test python -m pytest e2e-tests/ -v -m e2e
;;
all)
echo ""
echo "Running unit tests in Docker..."
docker run --rm claude-sdk-test \
python -m pytest tests/ -v
echo ""
if [ -n "$ANTHROPIC_API_KEY" ]; then
echo "Running e2e tests in Docker..."
docker run --rm -e ANTHROPIC_API_KEY \
claude-sdk-test python -m pytest e2e-tests/ -v -m e2e
else
echo "Skipping e2e tests (ANTHROPIC_API_KEY not set)"
fi
;;
*)
usage
;;
esac
echo ""
echo "Done!"

32
scripts/update_cli_version.py Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""Update Claude Code CLI version in _cli_version.py."""
import re
import sys
from pathlib import Path
def update_cli_version(new_version: str) -> None:
"""Update CLI version in _cli_version.py."""
# Update _cli_version.py
version_path = Path("src/claude_agent_sdk/_cli_version.py")
content = version_path.read_text()
content = re.sub(
r'__cli_version__ = "[^"]+"',
f'__cli_version__ = "{new_version}"',
content,
count=1,
flags=re.MULTILINE,
)
version_path.write_text(content)
print(f"Updated {version_path} to {new_version}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python scripts/update_cli_version.py <version>")
sys.exit(1)
update_cli_version(sys.argv[1])

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Update version in pyproject.toml and __init__.py files.""" """Update version in pyproject.toml and __init__.py files."""
import sys
import re import re
import sys
from pathlib import Path from pathlib import Path
@ -11,39 +11,39 @@ def update_version(new_version: str) -> None:
# Update pyproject.toml # Update pyproject.toml
pyproject_path = Path("pyproject.toml") pyproject_path = Path("pyproject.toml")
content = pyproject_path.read_text() content = pyproject_path.read_text()
# Only update the version field in [project] section # Only update the version field in [project] section
content = re.sub( content = re.sub(
r'^version = "[^"]*"', r'^version = "[^"]*"',
f'version = "{new_version}"', f'version = "{new_version}"',
content, content,
count=1, count=1,
flags=re.MULTILINE flags=re.MULTILINE,
) )
pyproject_path.write_text(content) pyproject_path.write_text(content)
print(f"Updated pyproject.toml to version {new_version}") print(f"Updated pyproject.toml to version {new_version}")
# Update __init__.py # Update _version.py
init_path = Path("src/claude_code_sdk/__init__.py") version_path = Path("src/claude_agent_sdk/_version.py")
content = init_path.read_text() content = version_path.read_text()
# Only update __version__ assignment # Only update __version__ assignment
content = re.sub( content = re.sub(
r'^__version__ = "[^"]*"', r'^__version__ = "[^"]*"',
f'__version__ = "{new_version}"', f'__version__ = "{new_version}"',
content, content,
count=1, count=1,
flags=re.MULTILINE flags=re.MULTILINE,
) )
init_path.write_text(content) version_path.write_text(content)
print(f"Updated __init__.py to version {new_version}") print(f"Updated _version.py to version {new_version}")
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) != 2: if len(sys.argv) != 2:
print("Usage: python scripts/update_version.py <version>") print("Usage: python scripts/update_version.py <version>")
sys.exit(1) sys.exit(1)
update_version(sys.argv[1]) update_version(sys.argv[1])

View file

@ -12,15 +12,20 @@ from ._errors import (
ProcessError, ProcessError,
) )
from ._internal.transport import Transport from ._internal.transport import Transport
from ._version import __version__
from .client import ClaudeSDKClient from .client import ClaudeSDKClient
from .query import query from .query import query
from .types import ( from .types import (
AgentDefinition,
AssistantMessage, AssistantMessage,
BaseHookInput,
CanUseTool, CanUseTool,
ClaudeCodeOptions, ClaudeAgentOptions,
ContentBlock, ContentBlock,
HookCallback, HookCallback,
HookContext, HookContext,
HookInput,
HookJSONOutput,
HookMatcher, HookMatcher,
McpSdkServerConfig, McpSdkServerConfig,
McpServerConfig, McpServerConfig,
@ -30,7 +35,18 @@ from .types import (
PermissionResultAllow, PermissionResultAllow,
PermissionResultDeny, PermissionResultDeny,
PermissionUpdate, PermissionUpdate,
PostToolUseHookInput,
PreCompactHookInput,
PreToolUseHookInput,
ResultMessage, ResultMessage,
SandboxIgnoreViolations,
SandboxNetworkConfig,
SandboxSettings,
SdkBeta,
SdkPluginConfig,
SettingSource,
StopHookInput,
SubagentStopHookInput,
SystemMessage, SystemMessage,
TextBlock, TextBlock,
ThinkingBlock, ThinkingBlock,
@ -38,6 +54,7 @@ from .types import (
ToolResultBlock, ToolResultBlock,
ToolUseBlock, ToolUseBlock,
UserMessage, UserMessage,
UserPromptSubmitHookInput,
) )
# MCP Server Support # MCP Server Support
@ -141,7 +158,7 @@ def create_sdk_mcp_server(
Returns: Returns:
McpSdkServerConfig: A configuration object that can be passed to McpSdkServerConfig: A configuration object that can be passed to
ClaudeCodeOptions.mcp_servers. This config contains the server ClaudeAgentOptions.mcp_servers. This config contains the server
instance and metadata needed for the SDK to route tool calls. instance and metadata needed for the SDK to route tool calls.
Example: Example:
@ -161,7 +178,7 @@ def create_sdk_mcp_server(
... ) ... )
>>> >>>
>>> # Use with Claude >>> # Use with Claude
>>> options = ClaudeCodeOptions( >>> options = ClaudeAgentOptions(
... mcp_servers={"calc": calculator}, ... mcp_servers={"calc": calculator},
... allowed_tools=["add", "multiply"] ... allowed_tools=["add", "multiply"]
... ) ... )
@ -188,10 +205,10 @@ def create_sdk_mcp_server(
See Also: See Also:
- tool(): Decorator for creating tool functions - tool(): Decorator for creating tool functions
- ClaudeCodeOptions: Configuration for using servers with query() - ClaudeAgentOptions: Configuration for using servers with query()
""" """
from mcp.server import Server from mcp.server import Server
from mcp.types import TextContent, Tool from mcp.types import ImageContent, TextContent, Tool
# Create MCP server instance # Create MCP server instance
server = Server(name, version=version) server = Server(name, version=version)
@ -202,7 +219,7 @@ def create_sdk_mcp_server(
tool_map = {tool_def.name: tool_def for tool_def in tools} tool_map = {tool_def.name: tool_def for tool_def in tools}
# Register list_tools handler to expose available tools # Register list_tools handler to expose available tools
@server.list_tools() # type: ignore[no-untyped-call,misc] @server.list_tools() # type: ignore[no-untyped-call,untyped-decorator]
async def list_tools() -> list[Tool]: async def list_tools() -> list[Tool]:
"""Return the list of available tools.""" """Return the list of available tools."""
tool_list = [] tool_list = []
@ -248,7 +265,7 @@ def create_sdk_mcp_server(
return tool_list return tool_list
# Register call_tool handler to execute tools # Register call_tool handler to execute tools
@server.call_tool() # type: ignore[misc] @server.call_tool() # type: ignore[untyped-decorator]
async def call_tool(name: str, arguments: dict[str, Any]) -> Any: async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
"""Execute a tool by name with given arguments.""" """Execute a tool by name with given arguments."""
if name not in tool_map: if name not in tool_map:
@ -261,11 +278,19 @@ def create_sdk_mcp_server(
# Convert result to MCP format # Convert result to MCP format
# The decorator expects us to return the content, not a CallToolResult # The decorator expects us to return the content, not a CallToolResult
# It will wrap our return value in CallToolResult # It will wrap our return value in CallToolResult
content = [] content: list[TextContent | ImageContent] = []
if "content" in result: if "content" in result:
for item in result["content"]: for item in result["content"]:
if item.get("type") == "text": if item.get("type") == "text":
content.append(TextContent(type="text", text=item["text"])) content.append(TextContent(type="text", text=item["text"]))
if item.get("type") == "image":
content.append(
ImageContent(
type="image",
data=item["data"],
mimeType=item["mimeType"],
)
)
# Return just the content list - the decorator wraps it # Return just the content list - the decorator wraps it
return content return content
@ -274,11 +299,10 @@ def create_sdk_mcp_server(
return McpSdkServerConfig(type="sdk", name=name, instance=server) return McpSdkServerConfig(type="sdk", name=name, instance=server)
__version__ = "0.0.23"
__all__ = [ __all__ = [
# Main exports # Main exports
"query", "query",
"__version__",
# Transport # Transport
"Transport", "Transport",
"ClaudeSDKClient", "ClaudeSDKClient",
@ -291,7 +315,7 @@ __all__ = [
"SystemMessage", "SystemMessage",
"ResultMessage", "ResultMessage",
"Message", "Message",
"ClaudeCodeOptions", "ClaudeAgentOptions",
"TextBlock", "TextBlock",
"ThinkingBlock", "ThinkingBlock",
"ToolUseBlock", "ToolUseBlock",
@ -304,9 +328,30 @@ __all__ = [
"PermissionResultAllow", "PermissionResultAllow",
"PermissionResultDeny", "PermissionResultDeny",
"PermissionUpdate", "PermissionUpdate",
# Hook support
"HookCallback", "HookCallback",
"HookContext", "HookContext",
"HookInput",
"BaseHookInput",
"PreToolUseHookInput",
"PostToolUseHookInput",
"UserPromptSubmitHookInput",
"StopHookInput",
"SubagentStopHookInput",
"PreCompactHookInput",
"HookJSONOutput",
"HookMatcher", "HookMatcher",
# Agent support
"AgentDefinition",
"SettingSource",
# Plugin support
"SdkPluginConfig",
# Beta support
"SdkBeta",
# Sandbox support
"SandboxSettings",
"SandboxNetworkConfig",
"SandboxIgnoreViolations",
# MCP Server Support # MCP Server Support
"create_sdk_mcp_server", "create_sdk_mcp_server",
"tool", "tool",

View file

@ -0,0 +1,3 @@
# Ignore bundled CLI binaries (downloaded during build)
claude
claude.exe

View file

@ -0,0 +1,3 @@
"""Bundled Claude Code CLI version."""
__cli_version__ = "2.0.74"

View file

@ -5,7 +5,7 @@ from dataclasses import replace
from typing import Any from typing import Any
from ..types import ( from ..types import (
ClaudeCodeOptions, ClaudeAgentOptions,
HookEvent, HookEvent,
HookMatcher, HookMatcher,
Message, Message,
@ -31,17 +31,19 @@ class InternalClient:
internal_hooks[event] = [] internal_hooks[event] = []
for matcher in matchers: for matcher in matchers:
# Convert HookMatcher to internal dict format # Convert HookMatcher to internal dict format
internal_matcher = { internal_matcher: dict[str, Any] = {
"matcher": matcher.matcher if hasattr(matcher, "matcher") else None, "matcher": matcher.matcher if hasattr(matcher, "matcher") else None,
"hooks": matcher.hooks if hasattr(matcher, "hooks") else [], "hooks": matcher.hooks if hasattr(matcher, "hooks") else [],
} }
if hasattr(matcher, "timeout") and matcher.timeout is not None:
internal_matcher["timeout"] = matcher.timeout
internal_hooks[event].append(internal_matcher) internal_hooks[event].append(internal_matcher)
return internal_hooks return internal_hooks
async def process_query( async def process_query(
self, self,
prompt: str | AsyncIterable[dict[str, Any]], prompt: str | AsyncIterable[dict[str, Any]],
options: ClaudeCodeOptions, options: ClaudeAgentOptions,
transport: Transport | None = None, transport: Transport | None = None,
) -> AsyncIterator[Message]: ) -> AsyncIterator[Message]:
"""Process a query through transport and Query.""" """Process a query through transport and Query."""
@ -71,7 +73,8 @@ class InternalClient:
chosen_transport = transport chosen_transport = transport
else: else:
chosen_transport = SubprocessCLITransport( chosen_transport = SubprocessCLITransport(
prompt=prompt, options=configured_options prompt=prompt,
options=configured_options,
) )
# Connect transport # Connect transport

View file

@ -48,6 +48,7 @@ def parse_message(data: dict[str, Any]) -> Message:
case "user": case "user":
try: try:
parent_tool_use_id = data.get("parent_tool_use_id") parent_tool_use_id = data.get("parent_tool_use_id")
uuid = data.get("uuid")
if isinstance(data["message"]["content"], list): if isinstance(data["message"]["content"], list):
user_content_blocks: list[ContentBlock] = [] user_content_blocks: list[ContentBlock] = []
for block in data["message"]["content"]: for block in data["message"]["content"]:
@ -74,10 +75,12 @@ def parse_message(data: dict[str, Any]) -> Message:
) )
return UserMessage( return UserMessage(
content=user_content_blocks, content=user_content_blocks,
uuid=uuid,
parent_tool_use_id=parent_tool_use_id, parent_tool_use_id=parent_tool_use_id,
) )
return UserMessage( return UserMessage(
content=data["message"]["content"], content=data["message"]["content"],
uuid=uuid,
parent_tool_use_id=parent_tool_use_id, parent_tool_use_id=parent_tool_use_id,
) )
except KeyError as e: except KeyError as e:
@ -120,6 +123,7 @@ def parse_message(data: dict[str, Any]) -> Message:
content=content_blocks, content=content_blocks,
model=data["message"]["model"], model=data["message"]["model"],
parent_tool_use_id=data.get("parent_tool_use_id"), parent_tool_use_id=data.get("parent_tool_use_id"),
error=data["message"].get("error"),
) )
except KeyError as e: except KeyError as e:
raise MessageParseError( raise MessageParseError(
@ -149,6 +153,7 @@ def parse_message(data: dict[str, Any]) -> Message:
total_cost_usd=data.get("total_cost_usd"), total_cost_usd=data.get("total_cost_usd"),
usage=data.get("usage"), usage=data.get("usage"),
result=data.get("result"), result=data.get("result"),
structured_output=data.get("structured_output"),
) )
except KeyError as e: except KeyError as e:
raise MessageParseError( raise MessageParseError(

View file

@ -31,6 +31,25 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _convert_hook_output_for_cli(hook_output: dict[str, Any]) -> dict[str, Any]:
"""Convert Python-safe field names to CLI-expected field names.
The Python SDK uses `async_` and `continue_` to avoid keyword conflicts,
but the CLI expects `async` and `continue`. This function performs the
necessary conversion.
"""
converted = {}
for key, value in hook_output.items():
# Convert Python-safe names to JavaScript names
if key == "async_":
converted["async"] = value
elif key == "continue_":
converted["continue"] = value
else:
converted[key] = value
return converted
class Query: class Query:
"""Handles bidirectional control protocol on top of Transport. """Handles bidirectional control protocol on top of Transport.
@ -53,6 +72,7 @@ class Query:
| None = None, | None = None,
hooks: dict[str, list[dict[str, Any]]] | None = None, hooks: dict[str, list[dict[str, Any]]] | None = None,
sdk_mcp_servers: dict[str, "McpServer"] | None = None, sdk_mcp_servers: dict[str, "McpServer"] | None = None,
initialize_timeout: float = 60.0,
): ):
"""Initialize Query with transport and callbacks. """Initialize Query with transport and callbacks.
@ -62,7 +82,9 @@ class Query:
can_use_tool: Optional callback for tool permission requests can_use_tool: Optional callback for tool permission requests
hooks: Optional hook configurations hooks: Optional hook configurations
sdk_mcp_servers: Optional SDK MCP server instances sdk_mcp_servers: Optional SDK MCP server instances
initialize_timeout: Timeout in seconds for the initialize request
""" """
self._initialize_timeout = initialize_timeout
self.transport = transport self.transport = transport
self.is_streaming_mode = is_streaming_mode self.is_streaming_mode = is_streaming_mode
self.can_use_tool = can_use_tool self.can_use_tool = can_use_tool
@ -85,6 +107,12 @@ class Query:
self._closed = False self._closed = False
self._initialization_result: dict[str, Any] | None = None self._initialization_result: dict[str, Any] | None = None
# Track first result for proper stream closure with SDK MCP servers
self._first_result_event = anyio.Event()
self._stream_close_timeout = (
float(os.environ.get("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "60000")) / 1000.0
) # Convert ms to seconds
async def initialize(self) -> dict[str, Any] | None: async def initialize(self) -> dict[str, Any] | None:
"""Initialize control protocol if in streaming mode. """Initialize control protocol if in streaming mode.
@ -107,12 +135,13 @@ class Query:
self.next_callback_id += 1 self.next_callback_id += 1
self.hook_callbacks[callback_id] = callback self.hook_callbacks[callback_id] = callback
callback_ids.append(callback_id) callback_ids.append(callback_id)
hooks_config[event].append( hook_matcher_config: dict[str, Any] = {
{ "matcher": matcher.get("matcher"),
"matcher": matcher.get("matcher"), "hookCallbackIds": callback_ids,
"hookCallbackIds": callback_ids, }
} if matcher.get("timeout") is not None:
) hook_matcher_config["timeout"] = matcher.get("timeout")
hooks_config[event].append(hook_matcher_config)
# Send initialize request # Send initialize request
request = { request = {
@ -120,7 +149,10 @@ class Query:
"hooks": hooks_config if hooks_config else None, "hooks": hooks_config if hooks_config else None,
} }
response = await self._send_control_request(request) # Use longer timeout for initialize since MCP servers may take time to start
response = await self._send_control_request(
request, timeout=self._initialize_timeout
)
self._initialized = True self._initialized = True
self._initialization_result = response # Store for later access self._initialization_result = response # Store for later access
return response return response
@ -169,6 +201,10 @@ class Query:
# TODO: Implement cancellation support # TODO: Implement cancellation support
continue continue
# Track results for proper stream closure
if msg_type == "result":
self._first_result_event.set()
# Regular SDK messages go to the stream # Regular SDK messages go to the stream
await self._message_send.send(message) await self._message_send.send(message)
@ -178,6 +214,11 @@ class Query:
raise # Re-raise to properly handle cancellation raise # Re-raise to properly handle cancellation
except Exception as e: except Exception as e:
logger.error(f"Fatal error in message reader: {e}") logger.error(f"Fatal error in message reader: {e}")
# Signal all pending control requests so they fail fast instead of timing out
for request_id, event in list(self.pending_control_responses.items()):
if request_id not in self.pending_control_results:
self.pending_control_results[request_id] = e
event.set()
# Put error in stream so iterators can handle it # Put error in stream so iterators can handle it
await self._message_send.send({"type": "error", "error": str(e)}) await self._message_send.send({"type": "error", "error": str(e)})
finally: finally:
@ -195,6 +236,7 @@ class Query:
if subtype == "can_use_tool": if subtype == "can_use_tool":
permission_request: SDKControlPermissionRequest = request_data # type: ignore[assignment] permission_request: SDKControlPermissionRequest = request_data # type: ignore[assignment]
original_input = permission_request["input"]
# Handle tool permission request # Handle tool permission request
if not self.can_use_tool: if not self.can_use_tool:
raise Exception("canUseTool callback is not provided") raise Exception("canUseTool callback is not provided")
@ -213,13 +255,23 @@ class Query:
# Convert PermissionResult to expected dict format # Convert PermissionResult to expected dict format
if isinstance(response, PermissionResultAllow): if isinstance(response, PermissionResultAllow):
response_data = {"allow": True} response_data = {
if response.updated_input is not None: "behavior": "allow",
response_data["input"] = response.updated_input "updatedInput": (
# TODO: Handle updatedPermissions when control protocol supports it response.updated_input
if response.updated_input is not None
else original_input
),
}
if response.updated_permissions is not None:
response_data["updatedPermissions"] = [
permission.to_dict()
for permission in response.updated_permissions
]
elif isinstance(response, PermissionResultDeny): elif isinstance(response, PermissionResultDeny):
response_data = {"allow": False, "reason": response.message} response_data = {"behavior": "deny", "message": response.message}
# TODO: Handle interrupt flag when control protocol supports it if response.interrupt:
response_data["interrupt"] = response.interrupt
else: else:
raise TypeError( raise TypeError(
f"Tool permission callback must return PermissionResult (PermissionResultAllow or PermissionResultDeny), got {type(response)}" f"Tool permission callback must return PermissionResult (PermissionResultAllow or PermissionResultDeny), got {type(response)}"
@ -233,11 +285,13 @@ class Query:
if not callback: if not callback:
raise Exception(f"No hook callback found for ID: {callback_id}") raise Exception(f"No hook callback found for ID: {callback_id}")
response_data = await callback( hook_output = await callback(
request_data.get("input"), request_data.get("input"),
request_data.get("tool_use_id"), request_data.get("tool_use_id"),
{"signal": None}, # TODO: Add abort signal support {"signal": None}, # TODO: Add abort signal support
) )
# Convert Python-safe field names (async_, continue_) to CLI-expected names (async, continue)
response_data = _convert_hook_output_for_cli(hook_output)
elif subtype == "mcp_message": elif subtype == "mcp_message":
# Handle SDK MCP request # Handle SDK MCP request
@ -282,8 +336,15 @@ class Query:
} }
await self.transport.write(json.dumps(error_response) + "\n") await self.transport.write(json.dumps(error_response) + "\n")
async def _send_control_request(self, request: dict[str, Any]) -> dict[str, Any]: async def _send_control_request(
"""Send control request to CLI and wait for response.""" self, request: dict[str, Any], timeout: float = 60.0
) -> dict[str, Any]:
"""Send control request to CLI and wait for response.
Args:
request: The control request to send
timeout: Timeout in seconds to wait for response (default 60s)
"""
if not self.is_streaming_mode: if not self.is_streaming_mode:
raise Exception("Control requests require streaming mode") raise Exception("Control requests require streaming mode")
@ -306,7 +367,7 @@ class Query:
# Wait for response # Wait for response
try: try:
with anyio.fail_after(60.0): with anyio.fail_after(timeout):
await event.wait() await event.wait()
result = self.pending_control_results.pop(request_id) result = self.pending_control_results.pop(request_id)
@ -469,14 +530,60 @@ class Query:
} }
) )
async def set_model(self, model: str | None) -> None:
"""Change the AI model."""
await self._send_control_request(
{
"subtype": "set_model",
"model": model,
}
)
async def rewind_files(self, user_message_id: str) -> None:
"""Rewind tracked files to their state at a specific user message.
Requires file checkpointing to be enabled via the `enable_file_checkpointing` option.
Args:
user_message_id: UUID of the user message to rewind to
"""
await self._send_control_request(
{
"subtype": "rewind_files",
"user_message_id": user_message_id,
}
)
async def stream_input(self, stream: AsyncIterable[dict[str, Any]]) -> None: async def stream_input(self, stream: AsyncIterable[dict[str, Any]]) -> None:
"""Stream input messages to transport.""" """Stream input messages to transport.
If SDK MCP servers or hooks are present, waits for the first result
before closing stdin to allow bidirectional control protocol communication.
"""
try: try:
async for message in stream: async for message in stream:
if self._closed: if self._closed:
break break
await self.transport.write(json.dumps(message) + "\n") await self.transport.write(json.dumps(message) + "\n")
# After all messages sent, end input
# If we have SDK MCP servers or hooks that need bidirectional communication,
# wait for first result before closing the channel
has_hooks = bool(self.hooks)
if self.sdk_mcp_servers or has_hooks:
logger.debug(
f"Waiting for first result before closing stdin "
f"(sdk_mcp_servers={len(self.sdk_mcp_servers)}, has_hooks={has_hooks})"
)
try:
with anyio.move_on_after(self._stream_close_timeout):
await self._first_result_event.wait()
logger.debug("Received first result, closing input stream")
except Exception:
logger.debug(
"Timed out waiting for first result, closing input stream"
)
# After all messages sent (and result received if needed), end input
await self.transport.end_input() await self.transport.end_input()
except Exception as e: except Exception as e:
logger.debug(f"Error streaming input: {e}") logger.debug(f"Error streaming input: {e}")

View file

@ -0,0 +1,672 @@
"""Subprocess transport implementation using Claude Code CLI."""
import json
import logging
import os
import platform
import re
import shutil
import sys
import tempfile
from collections.abc import AsyncIterable, AsyncIterator
from contextlib import suppress
from dataclasses import asdict
from pathlib import Path
from subprocess import PIPE
from typing import Any
import anyio
import anyio.abc
from anyio.abc import Process
from anyio.streams.text import TextReceiveStream, TextSendStream
from ..._errors import CLIConnectionError, CLINotFoundError, ProcessError
from ..._errors import CLIJSONDecodeError as SDKJSONDecodeError
from ..._version import __version__
from ...types import ClaudeAgentOptions
from . import Transport
logger = logging.getLogger(__name__)
_DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
MINIMUM_CLAUDE_CODE_VERSION = "2.0.0"
# Platform-specific command line length limits
# Windows cmd.exe has a limit of 8191 characters, use 8000 for safety
# Other platforms have much higher limits
_CMD_LENGTH_LIMIT = 8000 if platform.system() == "Windows" else 100000
class SubprocessCLITransport(Transport):
"""Subprocess transport using Claude Code CLI."""
def __init__(
self,
prompt: str | AsyncIterable[dict[str, Any]],
options: ClaudeAgentOptions,
):
self._prompt = prompt
self._is_streaming = not isinstance(prompt, str)
self._options = options
self._cli_path = (
str(options.cli_path) if options.cli_path is not None else self._find_cli()
)
self._cwd = str(options.cwd) if options.cwd else None
self._process: Process | None = None
self._stdout_stream: TextReceiveStream | None = None
self._stdin_stream: TextSendStream | None = None
self._stderr_stream: TextReceiveStream | None = None
self._stderr_task_group: anyio.abc.TaskGroup | None = None
self._ready = False
self._exit_error: Exception | None = None # Track process exit errors
self._max_buffer_size = (
options.max_buffer_size
if options.max_buffer_size is not None
else _DEFAULT_MAX_BUFFER_SIZE
)
self._temp_files: list[str] = [] # Track temporary files for cleanup
self._write_lock: anyio.Lock = anyio.Lock()
def _find_cli(self) -> str:
"""Find Claude Code CLI binary."""
# First, check for bundled CLI
bundled_cli = self._find_bundled_cli()
if bundled_cli:
return bundled_cli
# Fall back to system-wide search
if cli := shutil.which("claude"):
return cli
locations = [
Path.home() / ".npm-global/bin/claude",
Path("/usr/local/bin/claude"),
Path.home() / ".local/bin/claude",
Path.home() / "node_modules/.bin/claude",
Path.home() / ".yarn/bin/claude",
Path.home() / ".claude/local/claude",
]
for path in locations:
if path.exists() and path.is_file():
return str(path)
raise CLINotFoundError(
"Claude Code not found. Install with:\n"
" npm install -g @anthropic-ai/claude-code\n"
"\nIf already installed locally, try:\n"
' export PATH="$HOME/node_modules/.bin:$PATH"\n'
"\nOr provide the path via ClaudeAgentOptions:\n"
" ClaudeAgentOptions(cli_path='/path/to/claude')"
)
def _find_bundled_cli(self) -> str | None:
"""Find bundled CLI binary if it exists."""
# Determine the CLI binary name based on platform
cli_name = "claude.exe" if platform.system() == "Windows" else "claude"
# Get the path to the bundled CLI
# The _bundled directory is in the same package as this module
bundled_path = Path(__file__).parent.parent.parent / "_bundled" / cli_name
if bundled_path.exists() and bundled_path.is_file():
logger.info(f"Using bundled Claude Code CLI: {bundled_path}")
return str(bundled_path)
return None
def _build_settings_value(self) -> str | None:
"""Build settings value, merging sandbox settings if provided.
Returns the settings value as either:
- A JSON string (if sandbox is provided or settings is JSON)
- A file path (if only settings path is provided without sandbox)
- None if neither settings nor sandbox is provided
"""
has_settings = self._options.settings is not None
has_sandbox = self._options.sandbox is not None
if not has_settings and not has_sandbox:
return None
# If only settings path and no sandbox, pass through as-is
if has_settings and not has_sandbox:
return self._options.settings
# If we have sandbox settings, we need to merge into a JSON object
settings_obj: dict[str, Any] = {}
if has_settings:
assert self._options.settings is not None
settings_str = self._options.settings.strip()
# Check if settings is a JSON string or a file path
if settings_str.startswith("{") and settings_str.endswith("}"):
# Parse JSON string
try:
settings_obj = json.loads(settings_str)
except json.JSONDecodeError:
# If parsing fails, treat as file path
logger.warning(
f"Failed to parse settings as JSON, treating as file path: {settings_str}"
)
# Read the file
settings_path = Path(settings_str)
if settings_path.exists():
with settings_path.open(encoding="utf-8") as f:
settings_obj = json.load(f)
else:
# It's a file path - read and parse
settings_path = Path(settings_str)
if settings_path.exists():
with settings_path.open(encoding="utf-8") as f:
settings_obj = json.load(f)
else:
logger.warning(f"Settings file not found: {settings_path}")
# Merge sandbox settings
if has_sandbox:
settings_obj["sandbox"] = self._options.sandbox
return json.dumps(settings_obj)
def _build_command(self) -> list[str]:
"""Build CLI command with arguments."""
cmd = [self._cli_path, "--output-format", "stream-json", "--verbose"]
if self._options.system_prompt is None:
cmd.extend(["--system-prompt", ""])
elif isinstance(self._options.system_prompt, str):
cmd.extend(["--system-prompt", self._options.system_prompt])
else:
if (
self._options.system_prompt.get("type") == "preset"
and "append" in self._options.system_prompt
):
cmd.extend(
["--append-system-prompt", self._options.system_prompt["append"]]
)
# Handle tools option (base set of tools)
if self._options.tools is not None:
tools = self._options.tools
if isinstance(tools, list):
if len(tools) == 0:
cmd.extend(["--tools", ""])
else:
cmd.extend(["--tools", ",".join(tools)])
else:
# Preset object - 'claude_code' preset maps to 'default'
cmd.extend(["--tools", "default"])
if self._options.allowed_tools:
cmd.extend(["--allowedTools", ",".join(self._options.allowed_tools)])
if self._options.max_turns:
cmd.extend(["--max-turns", str(self._options.max_turns)])
if self._options.max_budget_usd is not None:
cmd.extend(["--max-budget-usd", str(self._options.max_budget_usd)])
if self._options.disallowed_tools:
cmd.extend(["--disallowedTools", ",".join(self._options.disallowed_tools)])
if self._options.model:
cmd.extend(["--model", self._options.model])
if self._options.fallback_model:
cmd.extend(["--fallback-model", self._options.fallback_model])
if self._options.betas:
cmd.extend(["--betas", ",".join(self._options.betas)])
if self._options.permission_prompt_tool_name:
cmd.extend(
["--permission-prompt-tool", self._options.permission_prompt_tool_name]
)
if self._options.permission_mode:
cmd.extend(["--permission-mode", self._options.permission_mode])
if self._options.continue_conversation:
cmd.append("--continue")
if self._options.resume:
cmd.extend(["--resume", self._options.resume])
# Handle settings and sandbox: merge sandbox into settings if both are provided
settings_value = self._build_settings_value()
if settings_value:
cmd.extend(["--settings", settings_value])
if self._options.add_dirs:
# Convert all paths to strings and add each directory
for directory in self._options.add_dirs:
cmd.extend(["--add-dir", str(directory)])
if self._options.mcp_servers:
if isinstance(self._options.mcp_servers, dict):
# Process all servers, stripping instance field from SDK servers
servers_for_cli: dict[str, Any] = {}
for name, config in self._options.mcp_servers.items():
if isinstance(config, dict) and config.get("type") == "sdk":
# For SDK servers, pass everything except the instance field
sdk_config: dict[str, object] = {
k: v for k, v in config.items() if k != "instance"
}
servers_for_cli[name] = sdk_config
else:
# For external servers, pass as-is
servers_for_cli[name] = config
# Pass all servers to CLI
if servers_for_cli:
cmd.extend(
[
"--mcp-config",
json.dumps({"mcpServers": servers_for_cli}),
]
)
else:
# String or Path format: pass directly as file path or JSON string
cmd.extend(["--mcp-config", str(self._options.mcp_servers)])
if self._options.include_partial_messages:
cmd.append("--include-partial-messages")
if self._options.fork_session:
cmd.append("--fork-session")
if self._options.agents:
agents_dict = {
name: {k: v for k, v in asdict(agent_def).items() if v is not None}
for name, agent_def in self._options.agents.items()
}
agents_json = json.dumps(agents_dict)
cmd.extend(["--agents", agents_json])
sources_value = (
",".join(self._options.setting_sources)
if self._options.setting_sources is not None
else ""
)
cmd.extend(["--setting-sources", sources_value])
# Add plugin directories
if self._options.plugins:
for plugin in self._options.plugins:
if plugin["type"] == "local":
cmd.extend(["--plugin-dir", plugin["path"]])
else:
raise ValueError(f"Unsupported plugin type: {plugin['type']}")
# Add extra args for future CLI flags
for flag, value in self._options.extra_args.items():
if value is None:
# Boolean flag without value
cmd.append(f"--{flag}")
else:
# Flag with value
cmd.extend([f"--{flag}", str(value)])
if self._options.max_thinking_tokens is not None:
cmd.extend(
["--max-thinking-tokens", str(self._options.max_thinking_tokens)]
)
# Extract schema from output_format structure if provided
# Expected: {"type": "json_schema", "schema": {...}}
if (
self._options.output_format is not None
and isinstance(self._options.output_format, dict)
and self._options.output_format.get("type") == "json_schema"
):
schema = self._options.output_format.get("schema")
if schema is not None:
cmd.extend(["--json-schema", json.dumps(schema)])
# Add prompt handling based on mode
# IMPORTANT: This must come AFTER all flags because everything after "--" is treated as arguments
if self._is_streaming:
# Streaming mode: use --input-format stream-json
cmd.extend(["--input-format", "stream-json"])
else:
# String mode: use --print with the prompt
cmd.extend(["--print", "--", str(self._prompt)])
# Check if command line is too long (Windows limitation)
cmd_str = " ".join(cmd)
if len(cmd_str) > _CMD_LENGTH_LIMIT and self._options.agents:
# Command is too long - use temp file for agents
# Find the --agents argument and replace its value with @filepath
try:
agents_idx = cmd.index("--agents")
agents_json_value = cmd[agents_idx + 1]
# Create a temporary file
# ruff: noqa: SIM115
temp_file = tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False, encoding="utf-8"
)
temp_file.write(agents_json_value)
temp_file.close()
# Track for cleanup
self._temp_files.append(temp_file.name)
# Replace agents JSON with @filepath reference
cmd[agents_idx + 1] = f"@{temp_file.name}"
logger.info(
f"Command line length ({len(cmd_str)}) exceeds limit ({_CMD_LENGTH_LIMIT}). "
f"Using temp file for --agents: {temp_file.name}"
)
except (ValueError, IndexError) as e:
# This shouldn't happen, but log it just in case
logger.warning(f"Failed to optimize command line length: {e}")
return cmd
async def connect(self) -> None:
"""Start subprocess."""
if self._process:
return
if not os.environ.get("CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK"):
await self._check_claude_version()
cmd = self._build_command()
try:
# Merge environment variables: system -> user -> SDK required
process_env = {
**os.environ,
**self._options.env, # User-provided env vars
"CLAUDE_CODE_ENTRYPOINT": "sdk-py",
"CLAUDE_AGENT_SDK_VERSION": __version__,
}
# Enable file checkpointing if requested
if self._options.enable_file_checkpointing:
process_env["CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING"] = "true"
if self._cwd:
process_env["PWD"] = self._cwd
# Pipe stderr if we have a callback OR debug mode is enabled
should_pipe_stderr = (
self._options.stderr is not None
or "debug-to-stderr" in self._options.extra_args
)
# For backward compat: use debug_stderr file object if no callback and debug is on
stderr_dest = PIPE if should_pipe_stderr else None
self._process = await anyio.open_process(
cmd,
stdin=PIPE,
stdout=PIPE,
stderr=stderr_dest,
cwd=self._cwd,
env=process_env,
user=self._options.user,
)
if self._process.stdout:
self._stdout_stream = TextReceiveStream(self._process.stdout)
# Setup stderr stream if piped
if should_pipe_stderr and self._process.stderr:
self._stderr_stream = TextReceiveStream(self._process.stderr)
# Start async task to read stderr
self._stderr_task_group = anyio.create_task_group()
await self._stderr_task_group.__aenter__()
self._stderr_task_group.start_soon(self._handle_stderr)
# Setup stdin for streaming mode
if self._is_streaming and self._process.stdin:
self._stdin_stream = TextSendStream(self._process.stdin)
elif not self._is_streaming and self._process.stdin:
# String mode: close stdin immediately
await self._process.stdin.aclose()
self._ready = True
except FileNotFoundError as e:
# Check if the error comes from the working directory or the CLI
if self._cwd and not Path(self._cwd).exists():
error = CLIConnectionError(
f"Working directory does not exist: {self._cwd}"
)
self._exit_error = error
raise error from e
error = CLINotFoundError(f"Claude Code not found at: {self._cli_path}")
self._exit_error = error
raise error from e
except Exception as e:
error = CLIConnectionError(f"Failed to start Claude Code: {e}")
self._exit_error = error
raise error from e
async def _handle_stderr(self) -> None:
"""Handle stderr stream - read and invoke callbacks."""
if not self._stderr_stream:
return
try:
async for line in self._stderr_stream:
line_str = line.rstrip()
if not line_str:
continue
# Call the stderr callback if provided
if self._options.stderr:
self._options.stderr(line_str)
# For backward compatibility: write to debug_stderr if in debug mode
elif (
"debug-to-stderr" in self._options.extra_args
and self._options.debug_stderr
):
self._options.debug_stderr.write(line_str + "\n")
if hasattr(self._options.debug_stderr, "flush"):
self._options.debug_stderr.flush()
except anyio.ClosedResourceError:
pass # Stream closed, exit normally
except Exception:
pass # Ignore other errors during stderr reading
async def close(self) -> None:
"""Close the transport and clean up resources."""
# Clean up temporary files first (before early return)
for temp_file in self._temp_files:
with suppress(Exception):
Path(temp_file).unlink(missing_ok=True)
self._temp_files.clear()
if not self._process:
self._ready = False
return
# Close stderr task group if active
if self._stderr_task_group:
with suppress(Exception):
self._stderr_task_group.cancel_scope.cancel()
await self._stderr_task_group.__aexit__(None, None, None)
self._stderr_task_group = None
# Close stdin stream (acquire lock to prevent race with concurrent writes)
async with self._write_lock:
self._ready = False # Set inside lock to prevent TOCTOU with write()
if self._stdin_stream:
with suppress(Exception):
await self._stdin_stream.aclose()
self._stdin_stream = None
if self._stderr_stream:
with suppress(Exception):
await self._stderr_stream.aclose()
self._stderr_stream = None
# Terminate and wait for process
if self._process.returncode is None:
with suppress(ProcessLookupError):
self._process.terminate()
# Wait for process to finish with timeout
with suppress(Exception):
# Just try to wait, but don't block if it fails
await self._process.wait()
self._process = None
self._stdout_stream = None
self._stdin_stream = None
self._stderr_stream = None
self._exit_error = None
async def write(self, data: str) -> None:
"""Write raw data to the transport."""
async with self._write_lock:
# All checks inside lock to prevent TOCTOU races with close()/end_input()
if not self._ready or not self._stdin_stream:
raise CLIConnectionError("ProcessTransport is not ready for writing")
if self._process and self._process.returncode is not None:
raise CLIConnectionError(
f"Cannot write to terminated process (exit code: {self._process.returncode})"
)
if self._exit_error:
raise CLIConnectionError(
f"Cannot write to process that exited with error: {self._exit_error}"
) from self._exit_error
try:
await self._stdin_stream.send(data)
except Exception as e:
self._ready = False
self._exit_error = CLIConnectionError(
f"Failed to write to process stdin: {e}"
)
raise self._exit_error from e
async def end_input(self) -> None:
"""End the input stream (close stdin)."""
async with self._write_lock:
if self._stdin_stream:
with suppress(Exception):
await self._stdin_stream.aclose()
self._stdin_stream = None
def read_messages(self) -> AsyncIterator[dict[str, Any]]:
"""Read and parse messages from the transport."""
return self._read_messages_impl()
async def _read_messages_impl(self) -> AsyncIterator[dict[str, Any]]:
"""Internal implementation of read_messages."""
if not self._process or not self._stdout_stream:
raise CLIConnectionError("Not connected")
json_buffer = ""
# Process stdout messages
try:
async for line in self._stdout_stream:
line_str = line.strip()
if not line_str:
continue
# Accumulate partial JSON until we can parse it
# Note: TextReceiveStream can truncate long lines, so we need to buffer
# and speculatively parse until we get a complete JSON object
json_lines = line_str.split("\n")
for json_line in json_lines:
json_line = json_line.strip()
if not json_line:
continue
# Keep accumulating partial JSON until we can parse it
json_buffer += json_line
if len(json_buffer) > self._max_buffer_size:
buffer_length = len(json_buffer)
json_buffer = ""
raise SDKJSONDecodeError(
f"JSON message exceeded maximum buffer size of {self._max_buffer_size} bytes",
ValueError(
f"Buffer size {buffer_length} exceeds limit {self._max_buffer_size}"
),
)
try:
data = json.loads(json_buffer)
json_buffer = ""
yield data
except json.JSONDecodeError:
# We are speculatively decoding the buffer until we get
# a full JSON object. If there is an actual issue, we
# raise an error after exceeding the configured limit.
continue
except anyio.ClosedResourceError:
pass
except GeneratorExit:
# Client disconnected
pass
# Check process completion and handle errors
try:
returncode = await self._process.wait()
except Exception:
returncode = -1
# Use exit code for error detection
if returncode is not None and returncode != 0:
self._exit_error = ProcessError(
f"Command failed with exit code {returncode}",
exit_code=returncode,
stderr="Check stderr output for details",
)
raise self._exit_error
async def _check_claude_version(self) -> None:
"""Check Claude Code version and warn if below minimum."""
version_process = None
try:
with anyio.fail_after(2): # 2 second timeout
version_process = await anyio.open_process(
[self._cli_path, "-v"],
stdout=PIPE,
stderr=PIPE,
)
if version_process.stdout:
stdout_bytes = await version_process.stdout.receive()
version_output = stdout_bytes.decode().strip()
match = re.match(r"([0-9]+\.[0-9]+\.[0-9]+)", version_output)
if match:
version = match.group(1)
version_parts = [int(x) for x in version.split(".")]
min_parts = [
int(x) for x in MINIMUM_CLAUDE_CODE_VERSION.split(".")
]
if version_parts < min_parts:
warning = (
f"Warning: Claude Code version {version} is unsupported in the Agent SDK. "
f"Minimum required version is {MINIMUM_CLAUDE_CODE_VERSION}. "
"Some features may not work correctly."
)
logger.warning(warning)
print(warning, file=sys.stderr)
except Exception:
pass
finally:
if version_process:
with suppress(Exception):
version_process.terminate()
with suppress(Exception):
await version_process.wait()
def is_ready(self) -> bool:
"""Check if transport is ready for communication."""
return self._ready

View file

@ -0,0 +1,3 @@
"""Version information for claude-agent-sdk."""
__version__ = "0.1.18"

View file

@ -6,8 +6,9 @@ from collections.abc import AsyncIterable, AsyncIterator
from dataclasses import replace from dataclasses import replace
from typing import Any from typing import Any
from . import Transport
from ._errors import CLIConnectionError from ._errors import CLIConnectionError
from .types import ClaudeCodeOptions, HookEvent, HookMatcher, Message, ResultMessage from .types import ClaudeAgentOptions, HookEvent, HookMatcher, Message, ResultMessage
class ClaudeSDKClient: class ClaudeSDKClient:
@ -51,12 +52,17 @@ class ClaudeSDKClient:
exist. exist.
""" """
def __init__(self, options: ClaudeCodeOptions | None = None): def __init__(
self,
options: ClaudeAgentOptions | None = None,
transport: Transport | None = None,
):
"""Initialize Claude SDK client.""" """Initialize Claude SDK client."""
if options is None: if options is None:
options = ClaudeCodeOptions() options = ClaudeAgentOptions()
self.options = options self.options = options
self._transport: Any | None = None self._custom_transport = transport
self._transport: Transport | None = None
self._query: Any | None = None self._query: Any | None = None
os.environ["CLAUDE_CODE_ENTRYPOINT"] = "sdk-py-client" os.environ["CLAUDE_CODE_ENTRYPOINT"] = "sdk-py-client"
@ -69,10 +75,12 @@ class ClaudeSDKClient:
internal_hooks[event] = [] internal_hooks[event] = []
for matcher in matchers: for matcher in matchers:
# Convert HookMatcher to internal dict format # Convert HookMatcher to internal dict format
internal_matcher = { internal_matcher: dict[str, Any] = {
"matcher": matcher.matcher if hasattr(matcher, "matcher") else None, "matcher": matcher.matcher if hasattr(matcher, "matcher") else None,
"hooks": matcher.hooks if hasattr(matcher, "hooks") else [], "hooks": matcher.hooks if hasattr(matcher, "hooks") else [],
} }
if hasattr(matcher, "timeout") and matcher.timeout is not None:
internal_matcher["timeout"] = matcher.timeout
internal_hooks[event].append(internal_matcher) internal_hooks[event].append(internal_matcher)
return internal_hooks return internal_hooks
@ -115,10 +123,14 @@ class ClaudeSDKClient:
else: else:
options = self.options options = self.options
self._transport = SubprocessCLITransport( # Use provided custom transport or create subprocess transport
prompt=actual_prompt, if self._custom_transport:
options=options, self._transport = self._custom_transport
) else:
self._transport = SubprocessCLITransport(
prompt=actual_prompt,
options=options,
)
await self._transport.connect() await self._transport.connect()
# Extract SDK MCP servers from options # Extract SDK MCP servers from options
@ -128,6 +140,13 @@ class ClaudeSDKClient:
if isinstance(config, dict) and config.get("type") == "sdk": if isinstance(config, dict) and config.get("type") == "sdk":
sdk_mcp_servers[name] = config["instance"] # type: ignore[typeddict-item] sdk_mcp_servers[name] = config["instance"] # type: ignore[typeddict-item]
# Calculate initialize timeout from CLAUDE_CODE_STREAM_CLOSE_TIMEOUT env var if set
# CLAUDE_CODE_STREAM_CLOSE_TIMEOUT is in milliseconds, convert to seconds
initialize_timeout_ms = int(
os.environ.get("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "60000")
)
initialize_timeout = max(initialize_timeout_ms / 1000.0, 60.0)
# Create Query to handle control protocol # Create Query to handle control protocol
self._query = Query( self._query = Query(
transport=self._transport, transport=self._transport,
@ -137,6 +156,7 @@ class ClaudeSDKClient:
if self.options.hooks if self.options.hooks
else None, else None,
sdk_mcp_servers=sdk_mcp_servers, sdk_mcp_servers=sdk_mcp_servers,
initialize_timeout=initialize_timeout,
) )
# Start reading messages and initialize # Start reading messages and initialize
@ -193,6 +213,86 @@ class ClaudeSDKClient:
raise CLIConnectionError("Not connected. Call connect() first.") raise CLIConnectionError("Not connected. Call connect() first.")
await self._query.interrupt() await self._query.interrupt()
async def set_permission_mode(self, mode: str) -> None:
"""Change permission mode during conversation (only works with streaming mode).
Args:
mode: The permission mode to set. Valid options:
- 'default': CLI prompts for dangerous tools
- 'acceptEdits': Auto-accept file edits
- 'bypassPermissions': Allow all tools (use with caution)
Example:
```python
async with ClaudeSDKClient() as client:
# Start with default permissions
await client.query("Help me analyze this codebase")
# Review mode done, switch to auto-accept edits
await client.set_permission_mode('acceptEdits')
await client.query("Now implement the fix we discussed")
```
"""
if not self._query:
raise CLIConnectionError("Not connected. Call connect() first.")
await self._query.set_permission_mode(mode)
async def set_model(self, model: str | None = None) -> None:
"""Change the AI model during conversation (only works with streaming mode).
Args:
model: The model to use, or None to use default. Examples:
- 'claude-sonnet-4-5'
- 'claude-opus-4-1-20250805'
- 'claude-opus-4-20250514'
Example:
```python
async with ClaudeSDKClient() as client:
# Start with default model
await client.query("Help me understand this problem")
# Switch to a different model for implementation
await client.set_model('claude-sonnet-4-5')
await client.query("Now implement the solution")
```
"""
if not self._query:
raise CLIConnectionError("Not connected. Call connect() first.")
await self._query.set_model(model)
async def rewind_files(self, user_message_id: str) -> None:
"""Rewind tracked files to their state at a specific user message.
Requires:
- `enable_file_checkpointing=True` to track file changes
- `extra_args={"replay-user-messages": None}` to receive UserMessage
objects with `uuid` in the response stream
Args:
user_message_id: UUID of the user message to rewind to. This should be
the `uuid` field from a `UserMessage` received during the conversation.
Example:
```python
options = ClaudeAgentOptions(
enable_file_checkpointing=True,
extra_args={"replay-user-messages": None},
)
async with ClaudeSDKClient(options) as client:
await client.query("Make some changes to my files")
async for msg in client.receive_response():
if isinstance(msg, UserMessage) and msg.uuid:
checkpoint_id = msg.uuid # Save this for later
# Later, rewind to that point
await client.rewind_files(checkpoint_id)
```
"""
if not self._query:
raise CLIConnectionError("Not connected. Call connect() first.")
await self._query.rewind_files(user_message_id)
async def get_server_info(self) -> dict[str, Any] | None: async def get_server_info(self) -> dict[str, Any] | None:
"""Get server initialization info including available commands and output styles. """Get server initialization info including available commands and output styles.

View file

@ -6,13 +6,13 @@ from typing import Any
from ._internal.client import InternalClient from ._internal.client import InternalClient
from ._internal.transport import Transport from ._internal.transport import Transport
from .types import ClaudeCodeOptions, Message from .types import ClaudeAgentOptions, Message
async def query( async def query(
*, *,
prompt: str | AsyncIterable[dict[str, Any]], prompt: str | AsyncIterable[dict[str, Any]],
options: ClaudeCodeOptions | None = None, options: ClaudeAgentOptions | None = None,
transport: Transport | None = None, transport: Transport | None = None,
) -> AsyncIterator[Message]: ) -> AsyncIterator[Message]:
""" """
@ -52,7 +52,7 @@ async def query(
"parent_tool_use_id": None, "parent_tool_use_id": None,
"session_id": "..." "session_id": "..."
} }
options: Optional configuration (defaults to ClaudeCodeOptions() if None). options: Optional configuration (defaults to ClaudeAgentOptions() if None).
Set options.permission_mode to control tool execution: Set options.permission_mode to control tool execution:
- 'default': CLI prompts for dangerous tools - 'default': CLI prompts for dangerous tools
- 'acceptEdits': Auto-accept file edits - 'acceptEdits': Auto-accept file edits
@ -77,7 +77,7 @@ async def query(
# Code generation with specific settings # Code generation with specific settings
async for message in query( async for message in query(
prompt="Create a Python web server", prompt="Create a Python web server",
options=ClaudeCodeOptions( options=ClaudeAgentOptions(
system_prompt="You are an expert Python developer", system_prompt="You are an expert Python developer",
cwd="/home/user/project" cwd="/home/user/project"
) )
@ -98,7 +98,7 @@ async def query(
Example - With custom transport: Example - With custom transport:
```python ```python
from claude_code_sdk import query, Transport from claude_agent_sdk import query, Transport
class MyCustomTransport(Transport): class MyCustomTransport(Transport):
# Implement custom transport logic # Implement custom transport logic
@ -114,7 +114,7 @@ async def query(
""" """
if options is None: if options is None:
options = ClaudeCodeOptions() options = ClaudeAgentOptions()
os.environ["CLAUDE_CODE_ENTRYPOINT"] = "sdk-py" os.environ["CLAUDE_CODE_ENTRYPOINT"] = "sdk-py"

View file

@ -0,0 +1,754 @@
"""Type definitions for Claude SDK."""
import sys
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, TypedDict
from typing_extensions import NotRequired
if TYPE_CHECKING:
from mcp.server import Server as McpServer
else:
# Runtime placeholder for forward reference resolution in Pydantic 2.12+
McpServer = Any
# Permission modes
PermissionMode = Literal["default", "acceptEdits", "plan", "bypassPermissions"]
# SDK Beta features - see https://docs.anthropic.com/en/api/beta-headers
SdkBeta = Literal["context-1m-2025-08-07"]
# Agent definitions
SettingSource = Literal["user", "project", "local"]
class SystemPromptPreset(TypedDict):
"""System prompt preset configuration."""
type: Literal["preset"]
preset: Literal["claude_code"]
append: NotRequired[str]
class ToolsPreset(TypedDict):
"""Tools preset configuration."""
type: Literal["preset"]
preset: Literal["claude_code"]
@dataclass
class AgentDefinition:
"""Agent definition configuration."""
description: str
prompt: str
tools: list[str] | None = None
model: Literal["sonnet", "opus", "haiku", "inherit"] | None = None
# Permission Update types (matching TypeScript SDK)
PermissionUpdateDestination = Literal[
"userSettings", "projectSettings", "localSettings", "session"
]
PermissionBehavior = Literal["allow", "deny", "ask"]
@dataclass
class PermissionRuleValue:
"""Permission rule value."""
tool_name: str
rule_content: str | None = None
@dataclass
class PermissionUpdate:
"""Permission update configuration."""
type: Literal[
"addRules",
"replaceRules",
"removeRules",
"setMode",
"addDirectories",
"removeDirectories",
]
rules: list[PermissionRuleValue] | None = None
behavior: PermissionBehavior | None = None
mode: PermissionMode | None = None
directories: list[str] | None = None
destination: PermissionUpdateDestination | None = None
def to_dict(self) -> dict[str, Any]:
"""Convert PermissionUpdate to dictionary format matching TypeScript control protocol."""
result: dict[str, Any] = {
"type": self.type,
}
# Add destination for all variants
if self.destination is not None:
result["destination"] = self.destination
# Handle different type variants
if self.type in ["addRules", "replaceRules", "removeRules"]:
# Rules-based variants require rules and behavior
if self.rules is not None:
result["rules"] = [
{
"toolName": rule.tool_name,
"ruleContent": rule.rule_content,
}
for rule in self.rules
]
if self.behavior is not None:
result["behavior"] = self.behavior
elif self.type == "setMode":
# Mode variant requires mode
if self.mode is not None:
result["mode"] = self.mode
elif self.type in ["addDirectories", "removeDirectories"]:
# Directory variants require directories
if self.directories is not None:
result["directories"] = self.directories
return result
# Tool callback types
@dataclass
class ToolPermissionContext:
"""Context information for tool permission callbacks."""
signal: Any | None = None # Future: abort signal support
suggestions: list[PermissionUpdate] = field(
default_factory=list
) # Permission suggestions from CLI
# Match TypeScript's PermissionResult structure
@dataclass
class PermissionResultAllow:
"""Allow permission result."""
behavior: Literal["allow"] = "allow"
updated_input: dict[str, Any] | None = None
updated_permissions: list[PermissionUpdate] | None = None
@dataclass
class PermissionResultDeny:
"""Deny permission result."""
behavior: Literal["deny"] = "deny"
message: str = ""
interrupt: bool = False
PermissionResult = PermissionResultAllow | PermissionResultDeny
CanUseTool = Callable[
[str, dict[str, Any], ToolPermissionContext], Awaitable[PermissionResult]
]
##### Hook types
# Supported hook event types. Due to setup limitations, the Python SDK does not
# support SessionStart, SessionEnd, and Notification hooks.
HookEvent = (
Literal["PreToolUse"]
| Literal["PostToolUse"]
| Literal["UserPromptSubmit"]
| Literal["Stop"]
| Literal["SubagentStop"]
| Literal["PreCompact"]
)
# Hook input types - strongly typed for each hook event
class BaseHookInput(TypedDict):
"""Base hook input fields present across many hook events."""
session_id: str
transcript_path: str
cwd: str
permission_mode: NotRequired[str]
class PreToolUseHookInput(BaseHookInput):
"""Input data for PreToolUse hook events."""
hook_event_name: Literal["PreToolUse"]
tool_name: str
tool_input: dict[str, Any]
class PostToolUseHookInput(BaseHookInput):
"""Input data for PostToolUse hook events."""
hook_event_name: Literal["PostToolUse"]
tool_name: str
tool_input: dict[str, Any]
tool_response: Any
class UserPromptSubmitHookInput(BaseHookInput):
"""Input data for UserPromptSubmit hook events."""
hook_event_name: Literal["UserPromptSubmit"]
prompt: str
class StopHookInput(BaseHookInput):
"""Input data for Stop hook events."""
hook_event_name: Literal["Stop"]
stop_hook_active: bool
class SubagentStopHookInput(BaseHookInput):
"""Input data for SubagentStop hook events."""
hook_event_name: Literal["SubagentStop"]
stop_hook_active: bool
class PreCompactHookInput(BaseHookInput):
"""Input data for PreCompact hook events."""
hook_event_name: Literal["PreCompact"]
trigger: Literal["manual", "auto"]
custom_instructions: str | None
# Union type for all hook inputs
HookInput = (
PreToolUseHookInput
| PostToolUseHookInput
| UserPromptSubmitHookInput
| StopHookInput
| SubagentStopHookInput
| PreCompactHookInput
)
# Hook-specific output types
class PreToolUseHookSpecificOutput(TypedDict):
"""Hook-specific output for PreToolUse events."""
hookEventName: Literal["PreToolUse"]
permissionDecision: NotRequired[Literal["allow", "deny", "ask"]]
permissionDecisionReason: NotRequired[str]
updatedInput: NotRequired[dict[str, Any]]
class PostToolUseHookSpecificOutput(TypedDict):
"""Hook-specific output for PostToolUse events."""
hookEventName: Literal["PostToolUse"]
additionalContext: NotRequired[str]
class UserPromptSubmitHookSpecificOutput(TypedDict):
"""Hook-specific output for UserPromptSubmit events."""
hookEventName: Literal["UserPromptSubmit"]
additionalContext: NotRequired[str]
class SessionStartHookSpecificOutput(TypedDict):
"""Hook-specific output for SessionStart events."""
hookEventName: Literal["SessionStart"]
additionalContext: NotRequired[str]
HookSpecificOutput = (
PreToolUseHookSpecificOutput
| PostToolUseHookSpecificOutput
| UserPromptSubmitHookSpecificOutput
| SessionStartHookSpecificOutput
)
# See https://docs.anthropic.com/en/docs/claude-code/hooks#advanced%3A-json-output
# for documentation of the output types.
#
# IMPORTANT: The Python SDK uses `async_` and `continue_` (with underscores) to avoid
# Python keyword conflicts. These fields are automatically converted to `async` and
# `continue` when sent to the CLI. You should use the underscore versions in your
# Python code.
class AsyncHookJSONOutput(TypedDict):
"""Async hook output that defers hook execution.
Fields:
async_: Set to True to defer hook execution. Note: This is converted to
"async" when sent to the CLI - use "async_" in your Python code.
asyncTimeout: Optional timeout in milliseconds for the async operation.
"""
async_: Literal[
True
] # Using async_ to avoid Python keyword (converted to "async" for CLI)
asyncTimeout: NotRequired[int]
class SyncHookJSONOutput(TypedDict):
"""Synchronous hook output with control and decision fields.
This defines the structure for hook callbacks to control execution and provide
feedback to Claude.
Common Control Fields:
continue_: Whether Claude should proceed after hook execution (default: True).
Note: This is converted to "continue" when sent to the CLI.
suppressOutput: Hide stdout from transcript mode (default: False).
stopReason: Message shown when continue is False.
Decision Fields:
decision: Set to "block" to indicate blocking behavior.
systemMessage: Warning message displayed to the user.
reason: Feedback message for Claude about the decision.
Hook-Specific Output:
hookSpecificOutput: Event-specific controls (e.g., permissionDecision for
PreToolUse, additionalContext for PostToolUse).
Note: The CLI documentation shows field names without underscores ("async", "continue"),
but Python code should use the underscore versions ("async_", "continue_") as they
are automatically converted.
"""
# Common control fields
continue_: NotRequired[
bool
] # Using continue_ to avoid Python keyword (converted to "continue" for CLI)
suppressOutput: NotRequired[bool]
stopReason: NotRequired[str]
# Decision fields
# Note: "approve" is deprecated for PreToolUse (use permissionDecision instead)
# For other hooks, only "block" is meaningful
decision: NotRequired[Literal["block"]]
systemMessage: NotRequired[str]
reason: NotRequired[str]
# Hook-specific outputs
hookSpecificOutput: NotRequired[HookSpecificOutput]
HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutput
class HookContext(TypedDict):
"""Context information for hook callbacks.
Fields:
signal: Reserved for future abort signal support. Currently always None.
"""
signal: Any | None # Future: abort signal support
HookCallback = Callable[
# HookCallback input parameters:
# - input: Strongly-typed hook input with discriminated unions based on hook_event_name
# - tool_use_id: Optional tool use identifier
# - context: Hook context with abort signal support (currently placeholder)
[HookInput, str | None, HookContext],
Awaitable[HookJSONOutput],
]
# Hook matcher configuration
@dataclass
class HookMatcher:
"""Hook matcher configuration."""
# See https://docs.anthropic.com/en/docs/claude-code/hooks#structure for the
# expected string value. For example, for PreToolUse, the matcher can be
# a tool name like "Bash" or a combination of tool names like
# "Write|MultiEdit|Edit".
matcher: str | None = None
# A list of Python functions with function signature HookCallback
hooks: list[HookCallback] = field(default_factory=list)
# Timeout in seconds for all hooks in this matcher (default: 60)
timeout: float | None = None
# MCP Server config
class McpStdioServerConfig(TypedDict):
"""MCP stdio server configuration."""
type: NotRequired[Literal["stdio"]] # Optional for backwards compatibility
command: str
args: NotRequired[list[str]]
env: NotRequired[dict[str, str]]
class McpSSEServerConfig(TypedDict):
"""MCP SSE server configuration."""
type: Literal["sse"]
url: str
headers: NotRequired[dict[str, str]]
class McpHttpServerConfig(TypedDict):
"""MCP HTTP server configuration."""
type: Literal["http"]
url: str
headers: NotRequired[dict[str, str]]
class McpSdkServerConfig(TypedDict):
"""SDK MCP server configuration."""
type: Literal["sdk"]
name: str
instance: "McpServer"
McpServerConfig = (
McpStdioServerConfig | McpSSEServerConfig | McpHttpServerConfig | McpSdkServerConfig
)
class SdkPluginConfig(TypedDict):
"""SDK plugin configuration.
Currently only local plugins are supported via the 'local' type.
"""
type: Literal["local"]
path: str
# Sandbox configuration types
class SandboxNetworkConfig(TypedDict, total=False):
"""Network configuration for sandbox.
Attributes:
allowUnixSockets: Unix socket paths accessible in sandbox (e.g., SSH agents).
allowAllUnixSockets: Allow all Unix sockets (less secure).
allowLocalBinding: Allow binding to localhost ports (macOS only).
httpProxyPort: HTTP proxy port if bringing your own proxy.
socksProxyPort: SOCKS5 proxy port if bringing your own proxy.
"""
allowUnixSockets: list[str]
allowAllUnixSockets: bool
allowLocalBinding: bool
httpProxyPort: int
socksProxyPort: int
class SandboxIgnoreViolations(TypedDict, total=False):
"""Violations to ignore in sandbox.
Attributes:
file: File paths for which violations should be ignored.
network: Network hosts for which violations should be ignored.
"""
file: list[str]
network: list[str]
class SandboxSettings(TypedDict, total=False):
"""Sandbox settings configuration.
This controls how Claude Code sandboxes bash commands for filesystem
and network isolation.
**Important:** Filesystem and network restrictions are configured via permission
rules, not via these sandbox settings:
- Filesystem read restrictions: Use Read deny rules
- Filesystem write restrictions: Use Edit allow/deny rules
- Network restrictions: Use WebFetch allow/deny rules
Attributes:
enabled: Enable bash sandboxing (macOS/Linux only). Default: False
autoAllowBashIfSandboxed: Auto-approve bash commands when sandboxed. Default: True
excludedCommands: Commands that should run outside the sandbox (e.g., ["git", "docker"])
allowUnsandboxedCommands: Allow commands to bypass sandbox via dangerouslyDisableSandbox.
When False, all commands must run sandboxed (or be in excludedCommands). Default: True
network: Network configuration for sandbox.
ignoreViolations: Violations to ignore.
enableWeakerNestedSandbox: Enable weaker sandbox for unprivileged Docker environments
(Linux only). Reduces security. Default: False
Example:
```python
sandbox_settings: SandboxSettings = {
"enabled": True,
"autoAllowBashIfSandboxed": True,
"excludedCommands": ["docker"],
"network": {
"allowUnixSockets": ["/var/run/docker.sock"],
"allowLocalBinding": True
}
}
```
"""
enabled: bool
autoAllowBashIfSandboxed: bool
excludedCommands: list[str]
allowUnsandboxedCommands: bool
network: SandboxNetworkConfig
ignoreViolations: SandboxIgnoreViolations
enableWeakerNestedSandbox: bool
# Content block types
@dataclass
class TextBlock:
"""Text content block."""
text: str
@dataclass
class ThinkingBlock:
"""Thinking content block."""
thinking: str
signature: str
@dataclass
class ToolUseBlock:
"""Tool use content block."""
id: str
name: str
input: dict[str, Any]
@dataclass
class ToolResultBlock:
"""Tool result content block."""
tool_use_id: str
content: str | list[dict[str, Any]] | None = None
is_error: bool | None = None
ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock
# Message types
AssistantMessageError = Literal[
"authentication_failed",
"billing_error",
"rate_limit",
"invalid_request",
"server_error",
"unknown",
]
@dataclass
class UserMessage:
"""User message."""
content: str | list[ContentBlock]
uuid: str | None = None
parent_tool_use_id: str | None = None
@dataclass
class AssistantMessage:
"""Assistant message with content blocks."""
content: list[ContentBlock]
model: str
parent_tool_use_id: str | None = None
error: AssistantMessageError | None = None
@dataclass
class SystemMessage:
"""System message with metadata."""
subtype: str
data: dict[str, Any]
@dataclass
class ResultMessage:
"""Result message with cost and usage information."""
subtype: str
duration_ms: int
duration_api_ms: int
is_error: bool
num_turns: int
session_id: str
total_cost_usd: float | None = None
usage: dict[str, Any] | None = None
result: str | None = None
structured_output: Any = None
@dataclass
class StreamEvent:
"""Stream event for partial message updates during streaming."""
uuid: str
session_id: str
event: dict[str, Any] # The raw Anthropic API stream event
parent_tool_use_id: str | None = None
Message = UserMessage | AssistantMessage | SystemMessage | ResultMessage | StreamEvent
@dataclass
class ClaudeAgentOptions:
"""Query options for Claude SDK."""
tools: list[str] | ToolsPreset | None = None
allowed_tools: list[str] = field(default_factory=list)
system_prompt: str | SystemPromptPreset | None = None
mcp_servers: dict[str, McpServerConfig] | str | Path = field(default_factory=dict)
permission_mode: PermissionMode | None = None
continue_conversation: bool = False
resume: str | None = None
max_turns: int | None = None
max_budget_usd: float | None = None
disallowed_tools: list[str] = field(default_factory=list)
model: str | None = None
fallback_model: str | None = None
# Beta features - see https://docs.anthropic.com/en/api/beta-headers
betas: list[SdkBeta] = field(default_factory=list)
permission_prompt_tool_name: str | None = None
cwd: str | Path | None = None
cli_path: str | Path | None = None
settings: str | None = None
add_dirs: list[str | Path] = field(default_factory=list)
env: dict[str, str] = field(default_factory=dict)
extra_args: dict[str, str | None] = field(
default_factory=dict
) # Pass arbitrary CLI flags
max_buffer_size: int | None = None # Max bytes when buffering CLI stdout
debug_stderr: Any = (
sys.stderr
) # Deprecated: File-like object for debug output. Use stderr callback instead.
stderr: Callable[[str], None] | None = None # Callback for stderr output from CLI
# Tool permission callback
can_use_tool: CanUseTool | None = None
# Hook configurations
hooks: dict[HookEvent, list[HookMatcher]] | None = None
user: str | None = None
# Partial message streaming support
include_partial_messages: bool = False
# When true resumed sessions will fork to a new session ID rather than
# continuing the previous session.
fork_session: bool = False
# Agent definitions for custom agents
agents: dict[str, AgentDefinition] | None = None
# Setting sources to load (user, project, local)
setting_sources: list[SettingSource] | None = None
# Sandbox configuration for bash command isolation.
# Filesystem and network restrictions are derived from permission rules (Read/Edit/WebFetch),
# not from these sandbox settings.
sandbox: SandboxSettings | None = None
# Plugin configurations for custom plugins
plugins: list[SdkPluginConfig] = field(default_factory=list)
# Max tokens for thinking blocks
max_thinking_tokens: int | None = None
# Output format for structured outputs (matches Messages API structure)
# Example: {"type": "json_schema", "schema": {"type": "object", "properties": {...}}}
output_format: dict[str, Any] | None = None
# Enable file checkpointing to track file changes during the session.
# When enabled, files can be rewound to their state at any user message
# using `ClaudeSDKClient.rewind_files()`.
enable_file_checkpointing: bool = False
# SDK Control Protocol
class SDKControlInterruptRequest(TypedDict):
subtype: Literal["interrupt"]
class SDKControlPermissionRequest(TypedDict):
subtype: Literal["can_use_tool"]
tool_name: str
input: dict[str, Any]
# TODO: Add PermissionUpdate type here
permission_suggestions: list[Any] | None
blocked_path: str | None
class SDKControlInitializeRequest(TypedDict):
subtype: Literal["initialize"]
hooks: dict[HookEvent, Any] | None
class SDKControlSetPermissionModeRequest(TypedDict):
subtype: Literal["set_permission_mode"]
# TODO: Add PermissionMode
mode: str
class SDKHookCallbackRequest(TypedDict):
subtype: Literal["hook_callback"]
callback_id: str
input: Any
tool_use_id: str | None
class SDKControlMcpMessageRequest(TypedDict):
subtype: Literal["mcp_message"]
server_name: str
message: Any
class SDKControlRewindFilesRequest(TypedDict):
subtype: Literal["rewind_files"]
user_message_id: str
class SDKControlRequest(TypedDict):
type: Literal["control_request"]
request_id: str
request: (
SDKControlInterruptRequest
| SDKControlPermissionRequest
| SDKControlInitializeRequest
| SDKControlSetPermissionModeRequest
| SDKHookCallbackRequest
| SDKControlMcpMessageRequest
| SDKControlRewindFilesRequest
)
class ControlResponse(TypedDict):
subtype: Literal["success"]
request_id: str
response: dict[str, Any] | None
class ControlErrorResponse(TypedDict):
subtype: Literal["error"]
request_id: str
error: str
class SDKControlResponse(TypedDict):
type: Literal["control_response"]
response: ControlResponse | ControlErrorResponse

View file

@ -1,376 +0,0 @@
"""Subprocess transport implementation using Claude Code CLI."""
import json
import logging
import os
import shutil
from collections.abc import AsyncIterable, AsyncIterator
from contextlib import suppress
from pathlib import Path
from subprocess import PIPE
from typing import Any
import anyio
from anyio.abc import Process
from anyio.streams.text import TextReceiveStream, TextSendStream
from ..._errors import CLIConnectionError, CLINotFoundError, ProcessError
from ..._errors import CLIJSONDecodeError as SDKJSONDecodeError
from ...types import ClaudeCodeOptions
from . import Transport
logger = logging.getLogger(__name__)
_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
class SubprocessCLITransport(Transport):
"""Subprocess transport using Claude Code CLI."""
def __init__(
self,
prompt: str | AsyncIterable[dict[str, Any]],
options: ClaudeCodeOptions,
cli_path: str | Path | None = None,
):
self._prompt = prompt
self._is_streaming = not isinstance(prompt, str)
self._options = options
self._cli_path = str(cli_path) if cli_path else self._find_cli()
self._cwd = str(options.cwd) if options.cwd else None
self._process: Process | None = None
self._stdout_stream: TextReceiveStream | None = None
self._stdin_stream: TextSendStream | None = None
self._ready = False
self._exit_error: Exception | None = None # Track process exit errors
def _find_cli(self) -> str:
"""Find Claude Code CLI binary."""
if cli := shutil.which("claude"):
return cli
locations = [
Path.home() / ".npm-global/bin/claude",
Path("/usr/local/bin/claude"),
Path.home() / ".local/bin/claude",
Path.home() / "node_modules/.bin/claude",
Path.home() / ".yarn/bin/claude",
]
for path in locations:
if path.exists() and path.is_file():
return str(path)
node_installed = shutil.which("node") is not None
if not node_installed:
error_msg = "Claude Code requires Node.js, which is not installed.\n\n"
error_msg += "Install Node.js from: https://nodejs.org/\n"
error_msg += "\nAfter installing Node.js, install Claude Code:\n"
error_msg += " npm install -g @anthropic-ai/claude-code"
raise CLINotFoundError(error_msg)
raise CLINotFoundError(
"Claude Code not found. Install with:\n"
" npm install -g @anthropic-ai/claude-code\n"
"\nIf already installed locally, try:\n"
' export PATH="$HOME/node_modules/.bin:$PATH"\n'
"\nOr specify the path when creating transport:\n"
" SubprocessCLITransport(..., cli_path='/path/to/claude')"
)
def _build_command(self) -> list[str]:
"""Build CLI command with arguments."""
cmd = [self._cli_path, "--output-format", "stream-json", "--verbose"]
if self._options.system_prompt:
cmd.extend(["--system-prompt", self._options.system_prompt])
if self._options.append_system_prompt:
cmd.extend(["--append-system-prompt", self._options.append_system_prompt])
if self._options.allowed_tools:
cmd.extend(["--allowedTools", ",".join(self._options.allowed_tools)])
if self._options.max_turns:
cmd.extend(["--max-turns", str(self._options.max_turns)])
if self._options.disallowed_tools:
cmd.extend(["--disallowedTools", ",".join(self._options.disallowed_tools)])
if self._options.model:
cmd.extend(["--model", self._options.model])
if self._options.permission_prompt_tool_name:
cmd.extend(
["--permission-prompt-tool", self._options.permission_prompt_tool_name]
)
if self._options.permission_mode:
cmd.extend(["--permission-mode", self._options.permission_mode])
if self._options.continue_conversation:
cmd.append("--continue")
if self._options.resume:
cmd.extend(["--resume", self._options.resume])
if self._options.settings:
cmd.extend(["--settings", self._options.settings])
if self._options.add_dirs:
# Convert all paths to strings and add each directory
for directory in self._options.add_dirs:
cmd.extend(["--add-dir", str(directory)])
if self._options.mcp_servers:
if isinstance(self._options.mcp_servers, dict):
# Process all servers, stripping instance field from SDK servers
servers_for_cli: dict[str, Any] = {}
for name, config in self._options.mcp_servers.items():
if isinstance(config, dict) and config.get("type") == "sdk":
# For SDK servers, pass everything except the instance field
sdk_config: dict[str, object] = {
k: v for k, v in config.items() if k != "instance"
}
servers_for_cli[name] = sdk_config
else:
# For external servers, pass as-is
servers_for_cli[name] = config
# Pass all servers to CLI
if servers_for_cli:
cmd.extend(
[
"--mcp-config",
json.dumps({"mcpServers": servers_for_cli}),
]
)
else:
# String or Path format: pass directly as file path or JSON string
cmd.extend(["--mcp-config", str(self._options.mcp_servers)])
if self._options.include_partial_messages:
cmd.append("--include-partial-messages")
# Add extra args for future CLI flags
for flag, value in self._options.extra_args.items():
if value is None:
# Boolean flag without value
cmd.append(f"--{flag}")
else:
# Flag with value
cmd.extend([f"--{flag}", str(value)])
# Add prompt handling based on mode
if self._is_streaming:
# Streaming mode: use --input-format stream-json
cmd.extend(["--input-format", "stream-json"])
else:
# String mode: use --print with the prompt
cmd.extend(["--print", "--", str(self._prompt)])
return cmd
async def connect(self) -> None:
"""Start subprocess."""
if self._process:
return
cmd = self._build_command()
try:
# Merge environment variables: system -> user -> SDK required
process_env = {
**os.environ,
**self._options.env, # User-provided env vars
"CLAUDE_CODE_ENTRYPOINT": "sdk-py",
}
if self._cwd:
process_env["PWD"] = self._cwd
# Only output stderr if customer explicitly requested debug output and provided a file object
stderr_dest = (
self._options.debug_stderr
if "debug-to-stderr" in self._options.extra_args
and self._options.debug_stderr
else None
)
self._process = await anyio.open_process(
cmd,
stdin=PIPE,
stdout=PIPE,
stderr=stderr_dest,
cwd=self._cwd,
env=process_env,
user=self._options.user,
)
if self._process.stdout:
self._stdout_stream = TextReceiveStream(self._process.stdout)
# Setup stdin for streaming mode
if self._is_streaming and self._process.stdin:
self._stdin_stream = TextSendStream(self._process.stdin)
elif not self._is_streaming and self._process.stdin:
# String mode: close stdin immediately
await self._process.stdin.aclose()
self._ready = True
except FileNotFoundError as e:
# Check if the error comes from the working directory or the CLI
if self._cwd and not Path(self._cwd).exists():
error = CLIConnectionError(
f"Working directory does not exist: {self._cwd}"
)
self._exit_error = error
raise error from e
error = CLINotFoundError(f"Claude Code not found at: {self._cli_path}")
self._exit_error = error
raise error from e
except Exception as e:
error = CLIConnectionError(f"Failed to start Claude Code: {e}")
self._exit_error = error
raise error from e
async def close(self) -> None:
"""Close the transport and clean up resources."""
self._ready = False
if not self._process:
return
# Close streams
if self._stdin_stream:
with suppress(Exception):
await self._stdin_stream.aclose()
self._stdin_stream = None
if self._process.stdin:
with suppress(Exception):
await self._process.stdin.aclose()
# Terminate and wait for process
if self._process.returncode is None:
with suppress(ProcessLookupError):
self._process.terminate()
# Wait for process to finish with timeout
with suppress(Exception):
# Just try to wait, but don't block if it fails
await self._process.wait()
self._process = None
self._stdout_stream = None
self._stdin_stream = None
self._exit_error = None
async def write(self, data: str) -> None:
"""Write raw data to the transport."""
# Check if ready (like TypeScript)
if not self._ready or not self._stdin_stream:
raise CLIConnectionError("ProcessTransport is not ready for writing")
# Check if process is still alive (like TypeScript)
if self._process and self._process.returncode is not None:
raise CLIConnectionError(
f"Cannot write to terminated process (exit code: {self._process.returncode})"
)
# Check for exit errors (like TypeScript)
if self._exit_error:
raise CLIConnectionError(
f"Cannot write to process that exited with error: {self._exit_error}"
) from self._exit_error
try:
await self._stdin_stream.send(data)
except Exception as e:
self._ready = False # Mark as not ready (like TypeScript)
self._exit_error = CLIConnectionError(
f"Failed to write to process stdin: {e}"
)
raise self._exit_error from e
async def end_input(self) -> None:
"""End the input stream (close stdin)."""
if self._stdin_stream:
with suppress(Exception):
await self._stdin_stream.aclose()
self._stdin_stream = None
def read_messages(self) -> AsyncIterator[dict[str, Any]]:
"""Read and parse messages from the transport."""
return self._read_messages_impl()
async def _read_messages_impl(self) -> AsyncIterator[dict[str, Any]]:
"""Internal implementation of read_messages."""
if not self._process or not self._stdout_stream:
raise CLIConnectionError("Not connected")
json_buffer = ""
# Process stdout messages
try:
async for line in self._stdout_stream:
line_str = line.strip()
if not line_str:
continue
# Accumulate partial JSON until we can parse it
# Note: TextReceiveStream can truncate long lines, so we need to buffer
# and speculatively parse until we get a complete JSON object
json_lines = line_str.split("\n")
for json_line in json_lines:
json_line = json_line.strip()
if not json_line:
continue
# Keep accumulating partial JSON until we can parse it
json_buffer += json_line
if len(json_buffer) > _MAX_BUFFER_SIZE:
json_buffer = ""
raise SDKJSONDecodeError(
f"JSON message exceeded maximum buffer size of {_MAX_BUFFER_SIZE} bytes",
ValueError(
f"Buffer size {len(json_buffer)} exceeds limit {_MAX_BUFFER_SIZE}"
),
)
try:
data = json.loads(json_buffer)
json_buffer = ""
yield data
except json.JSONDecodeError:
# We are speculatively decoding the buffer until we get
# a full JSON object. If there is an actual issue, we
# raise an error after _MAX_BUFFER_SIZE.
continue
except anyio.ClosedResourceError:
pass
except GeneratorExit:
# Client disconnected
pass
# Check process completion and handle errors
try:
returncode = await self._process.wait()
except Exception:
returncode = -1
# Use exit code for error detection
if returncode is not None and returncode != 0:
self._exit_error = ProcessError(
f"Command failed with exit code {returncode}",
exit_code=returncode,
stderr="Check stderr output for details",
)
raise self._exit_error
def is_ready(self) -> bool:
"""Check if transport is ready for communication."""
return self._ready

View file

@ -1,384 +0,0 @@
"""Type definitions for Claude SDK."""
import sys
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, TypedDict
from typing_extensions import NotRequired
if TYPE_CHECKING:
from mcp.server import Server as McpServer
# Permission modes
PermissionMode = Literal["default", "acceptEdits", "plan", "bypassPermissions"]
# Permission Update types (matching TypeScript SDK)
PermissionUpdateDestination = Literal[
"userSettings", "projectSettings", "localSettings", "session"
]
PermissionBehavior = Literal["allow", "deny", "ask"]
@dataclass
class PermissionRuleValue:
"""Permission rule value."""
tool_name: str
rule_content: str | None = None
@dataclass
class PermissionUpdate:
"""Permission update configuration."""
type: Literal[
"addRules",
"replaceRules",
"removeRules",
"setMode",
"addDirectories",
"removeDirectories",
]
rules: list[PermissionRuleValue] | None = None
behavior: PermissionBehavior | None = None
mode: PermissionMode | None = None
directories: list[str] | None = None
destination: PermissionUpdateDestination | None = None
# Tool callback types
@dataclass
class ToolPermissionContext:
"""Context information for tool permission callbacks."""
signal: Any | None = None # Future: abort signal support
suggestions: list[PermissionUpdate] = field(
default_factory=list
) # Permission suggestions from CLI
# Match TypeScript's PermissionResult structure
@dataclass
class PermissionResultAllow:
"""Allow permission result."""
behavior: Literal["allow"] = "allow"
updated_input: dict[str, Any] | None = None
updated_permissions: list[PermissionUpdate] | None = None
@dataclass
class PermissionResultDeny:
"""Deny permission result."""
behavior: Literal["deny"] = "deny"
message: str = ""
interrupt: bool = False
PermissionResult = PermissionResultAllow | PermissionResultDeny
CanUseTool = Callable[
[str, dict[str, Any], ToolPermissionContext], Awaitable[PermissionResult]
]
##### Hook types
# Supported hook event types. Due to setup limitations, the Python SDK does not
# support SessionStart, SessionEnd, and Notification hooks.
HookEvent = (
Literal["PreToolUse"]
| Literal["PostToolUse"]
| Literal["UserPromptSubmit"]
| Literal["Stop"]
| Literal["SubagentStop"]
| Literal["PreCompact"]
)
# See https://docs.anthropic.com/en/docs/claude-code/hooks#advanced%3A-json-output
# for documentation of the output types. Currently, "continue", "stopReason",
# and "suppressOutput" are not supported in the Python SDK.
class HookJSONOutput(TypedDict):
# Whether to block the action related to the hook.
decision: NotRequired[Literal["block"]]
# Optionally add a system message that is not visible to Claude but saved in
# the chat transcript.
systemMessage: NotRequired[str]
# See each hook's individual "Decision Control" section in the documentation
# for guidance.
hookSpecificOutput: NotRequired[Any]
@dataclass
class HookContext:
"""Context information for hook callbacks."""
signal: Any | None = None # Future: abort signal support
HookCallback = Callable[
# HookCallback input parameters:
# - input
# See https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input for
# the type of 'input', the first value.
# - tool_use_id
# - context
[dict[str, Any], str | None, HookContext],
Awaitable[HookJSONOutput],
]
# Hook matcher configuration
@dataclass
class HookMatcher:
"""Hook matcher configuration."""
# See https://docs.anthropic.com/en/docs/claude-code/hooks#structure for the
# expected string value. For example, for PreToolUse, the matcher can be
# a tool name like "Bash" or a combination of tool names like
# "Write|MultiEdit|Edit".
matcher: str | None = None
# A list of Python functions with function signature HookCallback
hooks: list[HookCallback] = field(default_factory=list)
# MCP Server config
class McpStdioServerConfig(TypedDict):
"""MCP stdio server configuration."""
type: NotRequired[Literal["stdio"]] # Optional for backwards compatibility
command: str
args: NotRequired[list[str]]
env: NotRequired[dict[str, str]]
class McpSSEServerConfig(TypedDict):
"""MCP SSE server configuration."""
type: Literal["sse"]
url: str
headers: NotRequired[dict[str, str]]
class McpHttpServerConfig(TypedDict):
"""MCP HTTP server configuration."""
type: Literal["http"]
url: str
headers: NotRequired[dict[str, str]]
class McpSdkServerConfig(TypedDict):
"""SDK MCP server configuration."""
type: Literal["sdk"]
name: str
instance: "McpServer"
McpServerConfig = (
McpStdioServerConfig | McpSSEServerConfig | McpHttpServerConfig | McpSdkServerConfig
)
# Content block types
@dataclass
class TextBlock:
"""Text content block."""
text: str
@dataclass
class ThinkingBlock:
"""Thinking content block."""
thinking: str
signature: str
@dataclass
class ToolUseBlock:
"""Tool use content block."""
id: str
name: str
input: dict[str, Any]
@dataclass
class ToolResultBlock:
"""Tool result content block."""
tool_use_id: str
content: str | list[dict[str, Any]] | None = None
is_error: bool | None = None
ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock
# Message types
@dataclass
class UserMessage:
"""User message."""
content: str | list[ContentBlock]
parent_tool_use_id: str | None = None
@dataclass
class AssistantMessage:
"""Assistant message with content blocks."""
content: list[ContentBlock]
model: str
parent_tool_use_id: str | None = None
@dataclass
class SystemMessage:
"""System message with metadata."""
subtype: str
data: dict[str, Any]
@dataclass
class ResultMessage:
"""Result message with cost and usage information."""
subtype: str
duration_ms: int
duration_api_ms: int
is_error: bool
num_turns: int
session_id: str
total_cost_usd: float | None = None
usage: dict[str, Any] | None = None
result: str | None = None
@dataclass
class StreamEvent:
"""Stream event for partial message updates during streaming."""
uuid: str
session_id: str
event: dict[str, Any] # The raw Anthropic API stream event
parent_tool_use_id: str | None = None
Message = UserMessage | AssistantMessage | SystemMessage | ResultMessage | StreamEvent
@dataclass
class ClaudeCodeOptions:
"""Query options for Claude SDK."""
allowed_tools: list[str] = field(default_factory=list)
system_prompt: str | None = None
append_system_prompt: str | None = None
mcp_servers: dict[str, McpServerConfig] | str | Path = field(default_factory=dict)
permission_mode: PermissionMode | None = None
continue_conversation: bool = False
resume: str | None = None
max_turns: int | None = None
disallowed_tools: list[str] = field(default_factory=list)
model: str | None = None
permission_prompt_tool_name: str | None = None
cwd: str | Path | None = None
settings: str | None = None
add_dirs: list[str | Path] = field(default_factory=list)
env: dict[str, str] = field(default_factory=dict)
extra_args: dict[str, str | None] = field(
default_factory=dict
) # Pass arbitrary CLI flags
debug_stderr: Any = (
sys.stderr
) # File-like object for debug output when debug-to-stderr is set
# Tool permission callback
can_use_tool: CanUseTool | None = None
# Hook configurations
hooks: dict[HookEvent, list[HookMatcher]] | None = None
user: str | None = None
# Partial message streaming support
include_partial_messages: bool = False
# SDK Control Protocol
class SDKControlInterruptRequest(TypedDict):
subtype: Literal["interrupt"]
class SDKControlPermissionRequest(TypedDict):
subtype: Literal["can_use_tool"]
tool_name: str
input: dict[str, Any]
# TODO: Add PermissionUpdate type here
permission_suggestions: list[Any] | None
blocked_path: str | None
class SDKControlInitializeRequest(TypedDict):
subtype: Literal["initialize"]
hooks: dict[HookEvent, Any] | None
class SDKControlSetPermissionModeRequest(TypedDict):
subtype: Literal["set_permission_mode"]
# TODO: Add PermissionMode
mode: str
class SDKHookCallbackRequest(TypedDict):
subtype: Literal["hook_callback"]
callback_id: str
input: Any
tool_use_id: str | None
class SDKControlMcpMessageRequest(TypedDict):
subtype: Literal["mcp_message"]
server_name: str
message: Any
class SDKControlRequest(TypedDict):
type: Literal["control_request"]
request_id: str
request: (
SDKControlInterruptRequest
| SDKControlPermissionRequest
| SDKControlInitializeRequest
| SDKControlSetPermissionModeRequest
| SDKHookCallbackRequest
| SDKControlMcpMessageRequest
)
class ControlResponse(TypedDict):
subtype: Literal["success"]
request_id: str
response: dict[str, Any] | None
class ControlErrorResponse(TypedDict):
subtype: Literal["error"]
request_id: str
error: str
class SDKControlResponse(TypedDict):
type: Literal["control_response"]
response: ControlResponse | ControlErrorResponse

View file

@ -4,8 +4,8 @@ from unittest.mock import AsyncMock, Mock, patch
import anyio import anyio
from claude_code_sdk import AssistantMessage, ClaudeCodeOptions, query from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, query
from claude_code_sdk.types import TextBlock from claude_agent_sdk.types import TextBlock
class TestQueryFunction: class TestQueryFunction:
@ -16,7 +16,7 @@ class TestQueryFunction:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.client.InternalClient.process_query" "claude_agent_sdk._internal.client.InternalClient.process_query"
) as mock_process: ) as mock_process:
# Mock the async generator # Mock the async generator
async def mock_generator(): async def mock_generator():
@ -41,7 +41,7 @@ class TestQueryFunction:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.client.InternalClient.process_query" "claude_agent_sdk._internal.client.InternalClient.process_query"
) as mock_process: ) as mock_process:
async def mock_generator(): async def mock_generator():
@ -52,7 +52,7 @@ class TestQueryFunction:
mock_process.return_value = mock_generator() mock_process.return_value = mock_generator()
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
allowed_tools=["Read", "Write"], allowed_tools=["Read", "Write"],
system_prompt="You are helpful", system_prompt="You are helpful",
permission_mode="acceptEdits", permission_mode="acceptEdits",
@ -76,7 +76,7 @@ class TestQueryFunction:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.client.SubprocessCLITransport" "claude_agent_sdk._internal.client.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = AsyncMock() mock_transport = AsyncMock()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -109,7 +109,7 @@ class TestQueryFunction:
mock_transport.write = AsyncMock() mock_transport.write = AsyncMock()
mock_transport.is_ready = Mock(return_value=True) mock_transport.is_ready = Mock(return_value=True)
options = ClaudeCodeOptions(cwd="/custom/path") options = ClaudeAgentOptions(cwd="/custom/path")
messages = [] messages = []
async for msg in query(prompt="test", options=options): async for msg in query(prompt="test", options=options):
messages.append(msg) messages.append(msg)

View file

@ -1,6 +1,6 @@
"""Tests for Claude SDK error handling.""" """Tests for Claude SDK error handling."""
from claude_code_sdk import ( from claude_agent_sdk import (
ClaudeSDKError, ClaudeSDKError,
CLIConnectionError, CLIConnectionError,
CLIJSONDecodeError, CLIJSONDecodeError,

View file

@ -8,14 +8,14 @@ from unittest.mock import AsyncMock, Mock, patch
import anyio import anyio
import pytest import pytest
from claude_code_sdk import ( from claude_agent_sdk import (
AssistantMessage, AssistantMessage,
ClaudeCodeOptions, ClaudeAgentOptions,
CLINotFoundError, CLINotFoundError,
ResultMessage, ResultMessage,
query, query,
) )
from claude_code_sdk.types import ToolUseBlock from claude_agent_sdk.types import ToolUseBlock
class TestIntegration: class TestIntegration:
@ -26,7 +26,7 @@ class TestIntegration:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.client.SubprocessCLITransport" "claude_agent_sdk._internal.client.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = AsyncMock() mock_transport = AsyncMock()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -84,7 +84,7 @@ class TestIntegration:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.client.SubprocessCLITransport" "claude_agent_sdk._internal.client.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = AsyncMock() mock_transport = AsyncMock()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -132,7 +132,7 @@ class TestIntegration:
messages = [] messages = []
async for msg in query( async for msg in query(
prompt="Read /test.txt", prompt="Read /test.txt",
options=ClaudeCodeOptions(allowed_tools=["Read"]), options=ClaudeAgentOptions(allowed_tools=["Read"]),
): ):
messages.append(msg) messages.append(msg)
@ -161,7 +161,7 @@ class TestIntegration:
async for _ in query(prompt="test"): async for _ in query(prompt="test"):
pass pass
assert "Claude Code requires Node.js" in str(exc_info.value) assert "Claude Code not found" in str(exc_info.value)
anyio.run(_test) anyio.run(_test)
@ -170,7 +170,7 @@ class TestIntegration:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.client.SubprocessCLITransport" "claude_agent_sdk._internal.client.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = AsyncMock() mock_transport = AsyncMock()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -202,7 +202,7 @@ class TestIntegration:
messages = [] messages = []
async for msg in query( async for msg in query(
prompt="Continue", prompt="Continue",
options=ClaudeCodeOptions(continue_conversation=True), options=ClaudeAgentOptions(continue_conversation=True),
): ):
messages.append(msg) messages.append(msg)
@ -212,3 +212,73 @@ class TestIntegration:
assert call_kwargs["options"].continue_conversation is True assert call_kwargs["options"].continue_conversation is True
anyio.run(_test) anyio.run(_test)
def test_max_budget_usd_option(self):
"""Test query with max_budget_usd option."""
async def _test():
with patch(
"claude_agent_sdk._internal.client.SubprocessCLITransport"
) as mock_transport_class:
mock_transport = AsyncMock()
mock_transport_class.return_value = mock_transport
# Mock the message stream that exceeds budget
async def mock_receive():
yield {
"type": "assistant",
"message": {
"role": "assistant",
"content": [
{"type": "text", "text": "Starting to read..."}
],
"model": "claude-opus-4-1-20250805",
},
}
yield {
"type": "result",
"subtype": "error_max_budget_usd",
"duration_ms": 500,
"duration_api_ms": 400,
"is_error": False,
"num_turns": 1,
"session_id": "test-session-budget",
"total_cost_usd": 0.0002,
"usage": {
"input_tokens": 100,
"output_tokens": 50,
},
}
mock_transport.read_messages = mock_receive
mock_transport.connect = AsyncMock()
mock_transport.close = AsyncMock()
mock_transport.end_input = AsyncMock()
mock_transport.write = AsyncMock()
mock_transport.is_ready = Mock(return_value=True)
# Run query with very small budget
messages = []
async for msg in query(
prompt="Read the readme",
options=ClaudeAgentOptions(max_budget_usd=0.0001),
):
messages.append(msg)
# Verify results
assert len(messages) == 2
# Check result message
assert isinstance(messages[1], ResultMessage)
assert messages[1].subtype == "error_max_budget_usd"
assert messages[1].is_error is False
assert messages[1].total_cost_usd == 0.0002
assert messages[1].total_cost_usd is not None
assert messages[1].total_cost_usd > 0
# Verify transport was created with max_budget_usd option
mock_transport_class.assert_called_once()
call_kwargs = mock_transport_class.call_args.kwargs
assert call_kwargs["options"].max_budget_usd == 0.0001
anyio.run(_test)

View file

@ -2,9 +2,9 @@
import pytest import pytest
from claude_code_sdk._errors import MessageParseError from claude_agent_sdk._errors import MessageParseError
from claude_code_sdk._internal.message_parser import parse_message from claude_agent_sdk._internal.message_parser import parse_message
from claude_code_sdk.types import ( from claude_agent_sdk.types import (
AssistantMessage, AssistantMessage,
ResultMessage, ResultMessage,
SystemMessage, SystemMessage,
@ -31,6 +31,21 @@ class TestMessageParser:
assert isinstance(message.content[0], TextBlock) assert isinstance(message.content[0], TextBlock)
assert message.content[0].text == "Hello" assert message.content[0].text == "Hello"
def test_parse_user_message_with_uuid(self):
"""Test parsing a user message with uuid field (issue #414).
The uuid field is needed for file checkpointing with rewind_files().
"""
data = {
"type": "user",
"uuid": "msg-abc123-def456",
"message": {"content": [{"type": "text", "text": "Hello"}]},
}
message = parse_message(data)
assert isinstance(message, UserMessage)
assert message.uuid == "msg-abc123-def456"
assert len(message.content) == 1
def test_parse_user_message_with_tool_use(self): def test_parse_user_message_with_tool_use(self):
"""Test parsing a user message with tool_use block.""" """Test parsing a user message with tool_use block."""
data = { data = {

View file

@ -4,12 +4,14 @@ This test file verifies that SDK MCP servers work correctly through the full sta
matching the TypeScript SDK test/sdk.test.ts pattern. matching the TypeScript SDK test/sdk.test.ts pattern.
""" """
import base64
from typing import Any from typing import Any
import pytest import pytest
from mcp.types import CallToolRequest, CallToolRequestParams
from claude_code_sdk import ( from claude_agent_sdk import (
ClaudeCodeOptions, ClaudeAgentOptions,
create_sdk_mcp_server, create_sdk_mcp_server,
tool, tool,
) )
@ -159,7 +161,7 @@ async def test_mixed_servers():
# Create configuration with both SDK and external servers # Create configuration with both SDK and external servers
external_server = {"type": "stdio", "command": "echo", "args": ["test"]} external_server = {"type": "stdio", "command": "echo", "args": ["test"]}
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
mcp_servers={"sdk": sdk_server, "external": external_server} mcp_servers={"sdk": sdk_server, "external": external_server}
) )
@ -191,3 +193,73 @@ async def test_server_creation():
# When no tools are provided, the handlers are not registered # When no tools are provided, the handlers are not registered
assert ListToolsRequest not in instance.request_handlers assert ListToolsRequest not in instance.request_handlers
@pytest.mark.asyncio
async def test_image_content_support():
"""Test that tools can return image content with base64 data."""
# Create sample base64 image data (a simple 1x1 pixel PNG)
png_data = base64.b64encode(
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
b"\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\tpHYs\x00\x00\x0b\x13"
b"\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x0cIDATx\x9cc```"
b"\x00\x00\x00\x04\x00\x01]U!\x1c\x00\x00\x00\x00IEND\xaeB`\x82"
).decode("utf-8")
# Track tool executions
tool_executions: list[dict[str, Any]] = []
# Create a tool that returns both text and image content
@tool(
"generate_chart", "Generates a chart and returns it as an image", {"title": str}
)
async def generate_chart(args: dict[str, Any]) -> dict[str, Any]:
tool_executions.append({"name": "generate_chart", "args": args})
return {
"content": [
{"type": "text", "text": f"Generated chart: {args['title']}"},
{
"type": "image",
"data": png_data,
"mimeType": "image/png",
},
]
}
server_config = create_sdk_mcp_server(
name="image-test-server", version="1.0.0", tools=[generate_chart]
)
# Get the server instance
server = server_config["instance"]
call_handler = server.request_handlers[CallToolRequest]
# Call the chart generation tool
chart_request = CallToolRequest(
method="tools/call",
params=CallToolRequestParams(
name="generate_chart", arguments={"title": "Sales Report"}
),
)
result = await call_handler(chart_request)
# Verify the result contains both text and image content
assert len(result.root.content) == 2
# Check text content
text_content = result.root.content[0]
assert text_content.type == "text"
assert text_content.text == "Generated chart: Sales Report"
# Check image content
image_content = result.root.content[1]
assert image_content.type == "image"
assert image_content.data == png_data
assert image_content.mimeType == "image/png"
# Verify the tool was executed correctly
assert len(tool_executions) == 1
assert tool_executions[0]["name"] == "generate_chart"
assert tool_executions[0]["args"]["title"] == "Sales Report"

View file

@ -10,9 +10,9 @@ from unittest.mock import AsyncMock, Mock, patch
import anyio import anyio
import pytest import pytest
from claude_code_sdk import ( from claude_agent_sdk import (
AssistantMessage, AssistantMessage,
ClaudeCodeOptions, ClaudeAgentOptions,
ClaudeSDKClient, ClaudeSDKClient,
CLIConnectionError, CLIConnectionError,
ResultMessage, ResultMessage,
@ -20,7 +20,7 @@ from claude_code_sdk import (
UserMessage, UserMessage,
query, query,
) )
from claude_code_sdk._internal.transport.subprocess_cli import SubprocessCLITransport from claude_agent_sdk._internal.transport.subprocess_cli import SubprocessCLITransport
def create_mock_transport(with_init_response=True): def create_mock_transport(with_init_response=True):
@ -115,7 +115,7 @@ class TestClaudeSDKClientStreaming:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -135,7 +135,7 @@ class TestClaudeSDKClientStreaming:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -159,7 +159,7 @@ class TestClaudeSDKClientStreaming:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -178,7 +178,7 @@ class TestClaudeSDKClientStreaming:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -206,7 +206,7 @@ class TestClaudeSDKClientStreaming:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -240,7 +240,7 @@ class TestClaudeSDKClientStreaming:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -279,7 +279,7 @@ class TestClaudeSDKClientStreaming:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -348,7 +348,7 @@ class TestClaudeSDKClientStreaming:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -430,7 +430,7 @@ class TestClaudeSDKClientStreaming:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -471,14 +471,14 @@ class TestClaudeSDKClientStreaming:
"""Test client initialization with options.""" """Test client initialization with options."""
async def _test(): async def _test():
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
cwd="/custom/path", cwd="/custom/path",
allowed_tools=["Read", "Write"], allowed_tools=["Read", "Write"],
system_prompt="Be helpful", system_prompt="Be helpful",
) )
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -497,7 +497,7 @@ class TestClaudeSDKClientStreaming:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -633,21 +633,28 @@ assert '"Second"' in stdin_messages[1]
print('{"type": "result", "subtype": "success", "duration_ms": 100, "duration_api_ms": 50, "is_error": false, "num_turns": 1, "session_id": "test", "total_cost_usd": 0.001}') print('{"type": "result", "subtype": "success", "duration_ms": 100, "duration_api_ms": 50, "is_error": false, "num_turns": 1, "session_id": "test", "total_cost_usd": 0.001}')
""") """)
Path(test_script).chmod(0o755) # Make script executable (Unix-style systems)
if sys.platform != "win32":
Path(test_script).chmod(0o755)
try: try:
# Mock _find_cli to return python executing our test script # Mock _find_cli to return the test script path directly
with patch.object( with patch.object(
SubprocessCLITransport, "_find_cli", return_value=sys.executable SubprocessCLITransport, "_find_cli", return_value=test_script
): ):
# Mock _build_command to add our test script as first argument # Mock _build_command to properly execute Python script
original_build_command = SubprocessCLITransport._build_command original_build_command = SubprocessCLITransport._build_command
def mock_build_command(self): def mock_build_command(self):
# Get original command # Get original command
cmd = original_build_command(self) cmd = original_build_command(self)
# Replace the CLI path with python + script # On Windows, we need to use python interpreter to run the script
cmd[0] = test_script if sys.platform == "win32":
# Replace first element with python interpreter and script
cmd[0:1] = [sys.executable, test_script]
else:
# On Unix, just use the script directly
cmd[0] = test_script
return cmd return cmd
with patch.object( with patch.object(
@ -699,7 +706,7 @@ class TestClaudeSDKClientEdgeCases:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
# Create a new mock transport for each call # Create a new mock transport for each call
mock_transport_class.side_effect = [ mock_transport_class.side_effect = [
@ -732,7 +739,7 @@ class TestClaudeSDKClientEdgeCases:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport
@ -751,7 +758,7 @@ class TestClaudeSDKClientEdgeCases:
async def _test(): async def _test():
with patch( with patch(
"claude_code_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport"
) as mock_transport_class: ) as mock_transport_class:
mock_transport = create_mock_transport() mock_transport = create_mock_transport()
mock_transport_class.return_value = mock_transport mock_transport_class.return_value = mock_transport

View file

@ -8,12 +8,21 @@ from unittest.mock import AsyncMock, MagicMock
import anyio import anyio
import pytest import pytest
from claude_code_sdk._errors import CLIJSONDecodeError from claude_agent_sdk._errors import CLIJSONDecodeError
from claude_code_sdk._internal.transport.subprocess_cli import ( from claude_agent_sdk._internal.transport.subprocess_cli import (
_MAX_BUFFER_SIZE, _DEFAULT_MAX_BUFFER_SIZE,
SubprocessCLITransport, SubprocessCLITransport,
) )
from claude_code_sdk.types import ClaudeCodeOptions from claude_agent_sdk.types import ClaudeAgentOptions
DEFAULT_CLI_PATH = "/usr/bin/claude"
def make_options(**kwargs: object) -> ClaudeAgentOptions:
"""Construct ClaudeAgentOptions with a default CLI path for tests."""
cli_path = kwargs.pop("cli_path", DEFAULT_CLI_PATH)
return ClaudeAgentOptions(cli_path=cli_path, **kwargs)
class MockTextReceiveStream: class MockTextReceiveStream:
@ -50,9 +59,7 @@ class TestSubprocessBuffering:
buffered_line = json.dumps(json_obj1) + "\n" + json.dumps(json_obj2) buffered_line = json.dumps(json_obj1) + "\n" + json.dumps(json_obj2)
transport = SubprocessCLITransport( transport = SubprocessCLITransport(prompt="test", options=make_options())
prompt="test", options=ClaudeCodeOptions(), cli_path="/usr/bin/claude"
)
mock_process = MagicMock() mock_process = MagicMock()
mock_process.returncode = None mock_process.returncode = None
@ -85,9 +92,7 @@ class TestSubprocessBuffering:
buffered_line = json.dumps(json_obj1) + "\n" + json.dumps(json_obj2) buffered_line = json.dumps(json_obj1) + "\n" + json.dumps(json_obj2)
transport = SubprocessCLITransport( transport = SubprocessCLITransport(prompt="test", options=make_options())
prompt="test", options=ClaudeCodeOptions(), cli_path="/usr/bin/claude"
)
mock_process = MagicMock() mock_process = MagicMock()
mock_process.returncode = None mock_process.returncode = None
@ -115,9 +120,7 @@ class TestSubprocessBuffering:
buffered_line = json.dumps(json_obj1) + "\n\n\n" + json.dumps(json_obj2) buffered_line = json.dumps(json_obj1) + "\n\n\n" + json.dumps(json_obj2)
transport = SubprocessCLITransport( transport = SubprocessCLITransport(prompt="test", options=make_options())
prompt="test", options=ClaudeCodeOptions(), cli_path="/usr/bin/claude"
)
mock_process = MagicMock() mock_process = MagicMock()
mock_process.returncode = None mock_process.returncode = None
@ -161,9 +164,7 @@ class TestSubprocessBuffering:
part2 = complete_json[100:250] part2 = complete_json[100:250]
part3 = complete_json[250:] part3 = complete_json[250:]
transport = SubprocessCLITransport( transport = SubprocessCLITransport(prompt="test", options=make_options())
prompt="test", options=ClaudeCodeOptions(), cli_path="/usr/bin/claude"
)
mock_process = MagicMock() mock_process = MagicMock()
mock_process.returncode = None mock_process.returncode = None
@ -209,9 +210,7 @@ class TestSubprocessBuffering:
for i in range(0, len(complete_json), chunk_size) for i in range(0, len(complete_json), chunk_size)
] ]
transport = SubprocessCLITransport( transport = SubprocessCLITransport(prompt="test", options=make_options())
prompt="test", options=ClaudeCodeOptions(), cli_path="/usr/bin/claude"
)
mock_process = MagicMock() mock_process = MagicMock()
mock_process.returncode = None mock_process.returncode = None
@ -237,11 +236,9 @@ class TestSubprocessBuffering:
"""Test that exceeding buffer size raises an appropriate error.""" """Test that exceeding buffer size raises an appropriate error."""
async def _test() -> None: async def _test() -> None:
huge_incomplete = '{"data": "' + "x" * (_MAX_BUFFER_SIZE + 1000) huge_incomplete = '{"data": "' + "x" * (_DEFAULT_MAX_BUFFER_SIZE + 1000)
transport = SubprocessCLITransport( transport = SubprocessCLITransport(prompt="test", options=make_options())
prompt="test", options=ClaudeCodeOptions(), cli_path="/usr/bin/claude"
)
mock_process = MagicMock() mock_process = MagicMock()
mock_process.returncode = None mock_process.returncode = None
@ -260,6 +257,33 @@ class TestSubprocessBuffering:
anyio.run(_test) anyio.run(_test)
def test_buffer_size_option(self) -> None:
"""Test that the configurable buffer size option is respected."""
async def _test() -> None:
custom_limit = 512
huge_incomplete = '{"data": "' + "x" * (custom_limit + 10)
transport = SubprocessCLITransport(
prompt="test",
options=make_options(max_buffer_size=custom_limit),
)
mock_process = MagicMock()
mock_process.returncode = None
mock_process.wait = AsyncMock(return_value=None)
transport._process = mock_process
transport._stdout_stream = MockTextReceiveStream([huge_incomplete])
transport._stderr_stream = MockTextReceiveStream([])
with pytest.raises(CLIJSONDecodeError) as exc_info:
async for _ in transport.read_messages():
pass
assert f"maximum buffer size of {custom_limit} bytes" in str(exc_info.value)
anyio.run(_test)
def test_mixed_complete_and_split_json(self) -> None: def test_mixed_complete_and_split_json(self) -> None:
"""Test handling a mix of complete and split JSON messages.""" """Test handling a mix of complete and split JSON messages."""
@ -281,9 +305,7 @@ class TestSubprocessBuffering:
large_json[3000:] + "\n" + msg3, large_json[3000:] + "\n" + msg3,
] ]
transport = SubprocessCLITransport( transport = SubprocessCLITransport(prompt="test", options=make_options())
prompt="test", options=ClaudeCodeOptions(), cli_path="/usr/bin/claude"
)
mock_process = MagicMock() mock_process = MagicMock()
mock_process.returncode = None mock_process.returncode = None

View file

@ -1,17 +1,21 @@
"""Tests for tool permission callbacks and hook callbacks.""" """Tests for tool permission callbacks and hook callbacks."""
import json
import pytest import pytest
from claude_code_sdk import ( from claude_agent_sdk import (
ClaudeCodeOptions, ClaudeAgentOptions,
HookContext, HookContext,
HookInput,
HookJSONOutput,
HookMatcher, HookMatcher,
PermissionResultAllow, PermissionResultAllow,
PermissionResultDeny, PermissionResultDeny,
ToolPermissionContext, ToolPermissionContext,
) )
from claude_code_sdk._internal.query import Query from claude_agent_sdk._internal.query import Query
from claude_code_sdk._internal.transport import Transport from claude_agent_sdk._internal.transport import Transport
class MockTransport(Transport): class MockTransport(Transport):
@ -90,7 +94,7 @@ class TestToolPermissionCallbacks:
# Check response was sent # Check response was sent
assert len(transport.written_messages) == 1 assert len(transport.written_messages) == 1
response = transport.written_messages[0] response = transport.written_messages[0]
assert '"allow": true' in response assert '"behavior": "allow"' in response
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_permission_callback_deny(self): async def test_permission_callback_deny(self):
@ -125,8 +129,8 @@ class TestToolPermissionCallbacks:
# Check response # Check response
assert len(transport.written_messages) == 1 assert len(transport.written_messages) == 1
response = transport.written_messages[0] response = transport.written_messages[0]
assert '"allow": false' in response assert '"behavior": "deny"' in response
assert '"reason": "Security policy violation"' in response assert '"message": "Security policy violation"' in response
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_permission_callback_input_modification(self): async def test_permission_callback_input_modification(self):
@ -164,7 +168,7 @@ class TestToolPermissionCallbacks:
# Check response includes modified input # Check response includes modified input
assert len(transport.written_messages) == 1 assert len(transport.written_messages) == 1
response = transport.written_messages[0] response = transport.written_messages[0]
assert '"allow": true' in response assert '"behavior": "allow"' in response
assert '"safe_mode": true' in response assert '"safe_mode": true' in response
@pytest.mark.asyncio @pytest.mark.asyncio
@ -213,7 +217,7 @@ class TestHookCallbacks:
hook_calls = [] hook_calls = []
async def test_hook( async def test_hook(
input_data: dict, tool_use_id: str | None, context: HookContext input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> dict: ) -> dict:
hook_calls.append({"input": input_data, "tool_use_id": tool_use_id}) hook_calls.append({"input": input_data, "tool_use_id": tool_use_id})
return {"processed": True} return {"processed": True}
@ -257,9 +261,204 @@ class TestHookCallbacks:
last_response = transport.written_messages[-1] last_response = transport.written_messages[-1]
assert '"processed": true' in last_response assert '"processed": true' in last_response
@pytest.mark.asyncio
async def test_hook_output_fields(self):
"""Test that all SyncHookJSONOutput fields are properly handled."""
class TestClaudeCodeOptionsIntegration: # Test all SyncHookJSONOutput fields together
"""Test that callbacks work through ClaudeCodeOptions.""" async def comprehensive_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
return {
# Control fields
"continue_": True,
"suppressOutput": False,
"stopReason": "Test stop reason",
# Decision fields
"decision": "block",
"systemMessage": "Test system message",
"reason": "Test reason for blocking",
# Hook-specific output with all PreToolUse fields
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Security policy violation",
"updatedInput": {"modified": "input"},
},
}
transport = MockTransport()
hooks = {
"PreToolUse": [
{"matcher": {"tool": "TestTool"}, "hooks": [comprehensive_hook]}
]
}
query = Query(
transport=transport, is_streaming_mode=True, can_use_tool=None, hooks=hooks
)
callback_id = "test_comprehensive_hook"
query.hook_callbacks[callback_id] = comprehensive_hook
request = {
"type": "control_request",
"request_id": "test-comprehensive",
"request": {
"subtype": "hook_callback",
"callback_id": callback_id,
"input": {"test": "data"},
"tool_use_id": "tool-456",
},
}
await query._handle_control_request(request)
# Check response contains all the fields
assert len(transport.written_messages) > 0
last_response = transport.written_messages[-1]
# Parse the JSON response
response_data = json.loads(last_response)
# The hook result is nested at response.response
result = response_data["response"]["response"]
# Verify control fields are present and converted to CLI format
assert result.get("continue") is True, (
"continue_ should be converted to continue"
)
assert "continue_" not in result, "continue_ should not appear in CLI output"
assert result.get("suppressOutput") is False
assert result.get("stopReason") == "Test stop reason"
# Verify decision fields are present
assert result.get("decision") == "block"
assert result.get("reason") == "Test reason for blocking"
assert result.get("systemMessage") == "Test system message"
# Verify hook-specific output is present
hook_output = result.get("hookSpecificOutput", {})
assert hook_output.get("hookEventName") == "PreToolUse"
assert hook_output.get("permissionDecision") == "deny"
assert (
hook_output.get("permissionDecisionReason") == "Security policy violation"
)
assert "updatedInput" in hook_output
@pytest.mark.asyncio
async def test_async_hook_output(self):
"""Test AsyncHookJSONOutput type with proper async fields."""
async def async_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
# Test that async hooks properly use async_ and asyncTimeout fields
return {
"async_": True,
"asyncTimeout": 5000,
}
transport = MockTransport()
hooks = {"PreToolUse": [{"matcher": None, "hooks": [async_hook]}]}
query = Query(
transport=transport, is_streaming_mode=True, can_use_tool=None, hooks=hooks
)
callback_id = "test_async_hook"
query.hook_callbacks[callback_id] = async_hook
request = {
"type": "control_request",
"request_id": "test-async",
"request": {
"subtype": "hook_callback",
"callback_id": callback_id,
"input": {"test": "async_data"},
"tool_use_id": None,
},
}
await query._handle_control_request(request)
# Check response contains async fields
assert len(transport.written_messages) > 0
last_response = transport.written_messages[-1]
# Parse the JSON response
response_data = json.loads(last_response)
# The hook result is nested at response.response
result = response_data["response"]["response"]
# The SDK should convert async_ to "async" for CLI compatibility
assert result.get("async") is True, "async_ should be converted to async"
assert "async_" not in result, "async_ should not appear in CLI output"
assert result.get("asyncTimeout") == 5000
@pytest.mark.asyncio
async def test_field_name_conversion(self):
"""Test that Python-safe field names (async_, continue_) are converted to CLI format (async, continue)."""
async def conversion_test_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
# Return both async_ and continue_ to test conversion
return {
"async_": True,
"asyncTimeout": 10000,
"continue_": False,
"stopReason": "Testing field conversion",
"systemMessage": "Fields should be converted",
}
transport = MockTransport()
hooks = {"PreToolUse": [{"matcher": None, "hooks": [conversion_test_hook]}]}
query = Query(
transport=transport, is_streaming_mode=True, can_use_tool=None, hooks=hooks
)
callback_id = "test_conversion"
query.hook_callbacks[callback_id] = conversion_test_hook
request = {
"type": "control_request",
"request_id": "test-conversion",
"request": {
"subtype": "hook_callback",
"callback_id": callback_id,
"input": {"test": "data"},
"tool_use_id": None,
},
}
await query._handle_control_request(request)
# Check response has converted field names
assert len(transport.written_messages) > 0
last_response = transport.written_messages[-1]
response_data = json.loads(last_response)
result = response_data["response"]["response"]
# Verify async_ was converted to async
assert result.get("async") is True, "async_ should be converted to async"
assert "async_" not in result, "async_ should not appear in output"
# Verify continue_ was converted to continue
assert result.get("continue") is False, (
"continue_ should be converted to continue"
)
assert "continue_" not in result, "continue_ should not appear in output"
# Verify other fields are unchanged
assert result.get("asyncTimeout") == 10000
assert result.get("stopReason") == "Testing field conversion"
assert result.get("systemMessage") == "Fields should be converted"
class TestClaudeAgentOptionsIntegration:
"""Test that callbacks work through ClaudeAgentOptions."""
def test_options_with_callbacks(self): def test_options_with_callbacks(self):
"""Test creating options with callbacks.""" """Test creating options with callbacks."""
@ -270,11 +469,11 @@ class TestClaudeCodeOptionsIntegration:
return PermissionResultAllow() return PermissionResultAllow()
async def my_hook( async def my_hook(
input_data: dict, tool_use_id: str | None, context: HookContext input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> dict: ) -> dict:
return {} return {}
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
can_use_tool=my_callback, can_use_tool=my_callback,
hooks={ hooks={
"tool_use_start": [ "tool_use_start": [

View file

@ -7,8 +7,17 @@ from unittest.mock import AsyncMock, MagicMock, patch
import anyio import anyio
import pytest import pytest
from claude_code_sdk._internal.transport.subprocess_cli import SubprocessCLITransport from claude_agent_sdk._internal.transport.subprocess_cli import SubprocessCLITransport
from claude_code_sdk.types import ClaudeCodeOptions from claude_agent_sdk.types import ClaudeAgentOptions
DEFAULT_CLI_PATH = "/usr/bin/claude"
def make_options(**kwargs: object) -> ClaudeAgentOptions:
"""Construct options using the standard CLI path unless overridden."""
cli_path = kwargs.pop("cli_path", DEFAULT_CLI_PATH)
return ClaudeAgentOptions(cli_path=cli_path, **kwargs)
class TestSubprocessCLITransport: class TestSubprocessCLITransport:
@ -16,22 +25,20 @@ class TestSubprocessCLITransport:
def test_find_cli_not_found(self): def test_find_cli_not_found(self):
"""Test CLI not found error.""" """Test CLI not found error."""
from claude_code_sdk._errors import CLINotFoundError from claude_agent_sdk._errors import CLINotFoundError
with ( with (
patch("shutil.which", return_value=None), patch("shutil.which", return_value=None),
patch("pathlib.Path.exists", return_value=False), patch("pathlib.Path.exists", return_value=False),
pytest.raises(CLINotFoundError) as exc_info, pytest.raises(CLINotFoundError) as exc_info,
): ):
SubprocessCLITransport(prompt="test", options=ClaudeCodeOptions()) SubprocessCLITransport(prompt="test", options=ClaudeAgentOptions())
assert "Claude Code requires Node.js" in str(exc_info.value) assert "Claude Code not found" in str(exc_info.value)
def test_build_command_basic(self): def test_build_command_basic(self):
"""Test building basic CLI command.""" """Test building basic CLI command."""
transport = SubprocessCLITransport( transport = SubprocessCLITransport(prompt="Hello", options=make_options())
prompt="Hello", options=ClaudeCodeOptions(), cli_path="/usr/bin/claude"
)
cmd = transport._build_command() cmd = transport._build_command()
assert cmd[0] == "/usr/bin/claude" assert cmd[0] == "/usr/bin/claude"
@ -39,72 +46,146 @@ class TestSubprocessCLITransport:
assert "stream-json" in cmd assert "stream-json" in cmd
assert "--print" in cmd assert "--print" in cmd
assert "Hello" in cmd assert "Hello" in cmd
assert "--system-prompt" in cmd
assert cmd[cmd.index("--system-prompt") + 1] == ""
def test_cli_path_accepts_pathlib_path(self): def test_cli_path_accepts_pathlib_path(self):
"""Test that cli_path accepts pathlib.Path objects.""" """Test that cli_path accepts pathlib.Path objects."""
from pathlib import Path from pathlib import Path
path = Path("/usr/bin/claude")
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="Hello", prompt="Hello",
options=ClaudeCodeOptions(), options=ClaudeAgentOptions(cli_path=path),
cli_path=Path("/usr/bin/claude"),
) )
assert transport._cli_path == "/usr/bin/claude" # Path object is converted to string, compare with str(path)
assert transport._cli_path == str(path)
def test_build_command_with_options(self): def test_build_command_with_system_prompt_string(self):
"""Test building CLI command with options.""" """Test building CLI command with system prompt as string."""
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=ClaudeCodeOptions( options=make_options(
system_prompt="Be helpful", system_prompt="Be helpful",
allowed_tools=["Read", "Write"],
disallowed_tools=["Bash"],
model="claude-3-5-sonnet",
permission_mode="acceptEdits",
max_turns=5,
), ),
cli_path="/usr/bin/claude",
) )
cmd = transport._build_command() cmd = transport._build_command()
assert "--system-prompt" in cmd assert "--system-prompt" in cmd
assert "Be helpful" in cmd assert "Be helpful" in cmd
def test_build_command_with_system_prompt_preset(self):
"""Test building CLI command with system prompt preset."""
transport = SubprocessCLITransport(
prompt="test",
options=make_options(
system_prompt={"type": "preset", "preset": "claude_code"},
),
)
cmd = transport._build_command()
assert "--system-prompt" not in cmd
assert "--append-system-prompt" not in cmd
def test_build_command_with_system_prompt_preset_and_append(self):
"""Test building CLI command with system prompt preset and append."""
transport = SubprocessCLITransport(
prompt="test",
options=make_options(
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": "Be concise.",
},
),
)
cmd = transport._build_command()
assert "--system-prompt" not in cmd
assert "--append-system-prompt" in cmd
assert "Be concise." in cmd
def test_build_command_with_options(self):
"""Test building CLI command with options."""
transport = SubprocessCLITransport(
prompt="test",
options=make_options(
allowed_tools=["Read", "Write"],
disallowed_tools=["Bash"],
model="claude-sonnet-4-5",
permission_mode="acceptEdits",
max_turns=5,
),
)
cmd = transport._build_command()
assert "--allowedTools" in cmd assert "--allowedTools" in cmd
assert "Read,Write" in cmd assert "Read,Write" in cmd
assert "--disallowedTools" in cmd assert "--disallowedTools" in cmd
assert "Bash" in cmd assert "Bash" in cmd
assert "--model" in cmd assert "--model" in cmd
assert "claude-3-5-sonnet" in cmd assert "claude-sonnet-4-5" in cmd
assert "--permission-mode" in cmd assert "--permission-mode" in cmd
assert "acceptEdits" in cmd assert "acceptEdits" in cmd
assert "--max-turns" in cmd assert "--max-turns" in cmd
assert "5" in cmd assert "5" in cmd
def test_build_command_with_fallback_model(self):
"""Test building CLI command with fallback_model option."""
transport = SubprocessCLITransport(
prompt="test",
options=make_options(
model="opus",
fallback_model="sonnet",
),
)
cmd = transport._build_command()
assert "--model" in cmd
assert "opus" in cmd
assert "--fallback-model" in cmd
assert "sonnet" in cmd
def test_build_command_with_max_thinking_tokens(self):
"""Test building CLI command with max_thinking_tokens option."""
transport = SubprocessCLITransport(
prompt="test",
options=make_options(max_thinking_tokens=5000),
)
cmd = transport._build_command()
assert "--max-thinking-tokens" in cmd
assert "5000" in cmd
def test_build_command_with_add_dirs(self): def test_build_command_with_add_dirs(self):
"""Test building CLI command with add_dirs option.""" """Test building CLI command with add_dirs option."""
from pathlib import Path from pathlib import Path
dir1 = "/path/to/dir1"
dir2 = Path("/path/to/dir2")
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=ClaudeCodeOptions( options=make_options(add_dirs=[dir1, dir2]),
add_dirs=["/path/to/dir1", Path("/path/to/dir2")]
),
cli_path="/usr/bin/claude",
) )
cmd = transport._build_command() cmd = transport._build_command()
cmd_str = " ".join(cmd)
# Check that the command string contains the expected --add-dir flags # Check that both directories are in the command
assert "--add-dir /path/to/dir1 --add-dir /path/to/dir2" in cmd_str assert "--add-dir" in cmd
add_dir_indices = [i for i, x in enumerate(cmd) if x == "--add-dir"]
assert len(add_dir_indices) == 2
# The directories should appear after --add-dir flags
dirs_in_cmd = [cmd[i + 1] for i in add_dir_indices]
assert dir1 in dirs_in_cmd
assert str(dir2) in dirs_in_cmd
def test_session_continuation(self): def test_session_continuation(self):
"""Test session continuation options.""" """Test session continuation options."""
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="Continue from before", prompt="Continue from before",
options=ClaudeCodeOptions(continue_conversation=True, resume="session-123"), options=make_options(continue_conversation=True, resume="session-123"),
cli_path="/usr/bin/claude",
) )
cmd = transport._build_command() cmd = transport._build_command()
@ -117,6 +198,16 @@ class TestSubprocessCLITransport:
async def _test(): async def _test():
with patch("anyio.open_process") as mock_exec: with patch("anyio.open_process") as mock_exec:
# Mock version check process
mock_version_process = MagicMock()
mock_version_process.stdout = MagicMock()
mock_version_process.stdout.receive = AsyncMock(
return_value=b"2.0.0 (Claude Code)"
)
mock_version_process.terminate = MagicMock()
mock_version_process.wait = AsyncMock()
# Mock main process
mock_process = MagicMock() mock_process = MagicMock()
mock_process.returncode = None mock_process.returncode = None
mock_process.terminate = MagicMock() mock_process.terminate = MagicMock()
@ -129,12 +220,12 @@ class TestSubprocessCLITransport:
mock_stdin.aclose = AsyncMock() mock_stdin.aclose = AsyncMock()
mock_process.stdin = mock_stdin mock_process.stdin = mock_stdin
mock_exec.return_value = mock_process # Return version process first, then main process
mock_exec.side_effect = [mock_version_process, mock_process]
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=ClaudeCodeOptions(), options=make_options(),
cli_path="/usr/bin/claude",
) )
await transport.connect() await transport.connect()
@ -150,9 +241,7 @@ class TestSubprocessCLITransport:
"""Test reading messages from CLI output.""" """Test reading messages from CLI output."""
# This test is simplified to just test the transport creation # This test is simplified to just test the transport creation
# The full async stream handling is tested in integration tests # The full async stream handling is tested in integration tests
transport = SubprocessCLITransport( transport = SubprocessCLITransport(prompt="test", options=make_options())
prompt="test", options=ClaudeCodeOptions(), cli_path="/usr/bin/claude"
)
# The transport now just provides raw message reading via read_messages() # The transport now just provides raw message reading via read_messages()
# So we just verify the transport can be created and basic structure is correct # So we just verify the transport can be created and basic structure is correct
@ -161,13 +250,12 @@ class TestSubprocessCLITransport:
def test_connect_with_nonexistent_cwd(self): def test_connect_with_nonexistent_cwd(self):
"""Test that connect raises CLIConnectionError when cwd doesn't exist.""" """Test that connect raises CLIConnectionError when cwd doesn't exist."""
from claude_code_sdk._errors import CLIConnectionError from claude_agent_sdk._errors import CLIConnectionError
async def _test(): async def _test():
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=ClaudeCodeOptions(cwd="/this/directory/does/not/exist"), options=make_options(cwd="/this/directory/does/not/exist"),
cli_path="/usr/bin/claude",
) )
with pytest.raises(CLIConnectionError) as exc_info: with pytest.raises(CLIConnectionError) as exc_info:
@ -181,8 +269,7 @@ class TestSubprocessCLITransport:
"""Test building CLI command with settings as file path.""" """Test building CLI command with settings as file path."""
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=ClaudeCodeOptions(settings="/path/to/settings.json"), options=make_options(settings="/path/to/settings.json"),
cli_path="/usr/bin/claude",
) )
cmd = transport._build_command() cmd = transport._build_command()
@ -194,8 +281,7 @@ class TestSubprocessCLITransport:
settings_json = '{"permissions": {"allow": ["Bash(ls:*)"]}}' settings_json = '{"permissions": {"allow": ["Bash(ls:*)"]}}'
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=ClaudeCodeOptions(settings=settings_json), options=make_options(settings=settings_json),
cli_path="/usr/bin/claude",
) )
cmd = transport._build_command() cmd = transport._build_command()
@ -206,14 +292,13 @@ class TestSubprocessCLITransport:
"""Test building CLI command with extra_args for future flags.""" """Test building CLI command with extra_args for future flags."""
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=ClaudeCodeOptions( options=make_options(
extra_args={ extra_args={
"new-flag": "value", "new-flag": "value",
"boolean-flag": None, "boolean-flag": None,
"another-option": "test-value", "another-option": "test-value",
} }
), ),
cli_path="/usr/bin/claude",
) )
cmd = transport._build_command() cmd = transport._build_command()
@ -244,8 +329,7 @@ class TestSubprocessCLITransport:
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=ClaudeCodeOptions(mcp_servers=mcp_servers), options=make_options(mcp_servers=mcp_servers),
cli_path="/usr/bin/claude",
) )
cmd = transport._build_command() cmd = transport._build_command()
@ -265,36 +349,36 @@ class TestSubprocessCLITransport:
from pathlib import Path from pathlib import Path
# Test with string path # Test with string path
string_path = "/path/to/mcp-config.json"
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=ClaudeCodeOptions(mcp_servers="/path/to/mcp-config.json"), options=make_options(mcp_servers=string_path),
cli_path="/usr/bin/claude",
) )
cmd = transport._build_command() cmd = transport._build_command()
assert "--mcp-config" in cmd assert "--mcp-config" in cmd
mcp_idx = cmd.index("--mcp-config") mcp_idx = cmd.index("--mcp-config")
assert cmd[mcp_idx + 1] == "/path/to/mcp-config.json" assert cmd[mcp_idx + 1] == string_path
# Test with Path object # Test with Path object
path_obj = Path("/path/to/mcp-config.json")
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=ClaudeCodeOptions(mcp_servers=Path("/path/to/mcp-config.json")), options=make_options(mcp_servers=path_obj),
cli_path="/usr/bin/claude",
) )
cmd = transport._build_command() cmd = transport._build_command()
assert "--mcp-config" in cmd assert "--mcp-config" in cmd
mcp_idx = cmd.index("--mcp-config") mcp_idx = cmd.index("--mcp-config")
assert cmd[mcp_idx + 1] == "/path/to/mcp-config.json" # Path object gets converted to string, compare with str(path_obj)
assert cmd[mcp_idx + 1] == str(path_obj)
def test_build_command_with_mcp_servers_as_json_string(self): def test_build_command_with_mcp_servers_as_json_string(self):
"""Test building CLI command with mcp_servers as JSON string.""" """Test building CLI command with mcp_servers as JSON string."""
json_config = '{"mcpServers": {"server": {"type": "stdio", "command": "test"}}}' json_config = '{"mcpServers": {"server": {"type": "stdio", "command": "test"}}}'
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=ClaudeCodeOptions(mcp_servers=json_config), options=make_options(mcp_servers=json_config),
cli_path="/usr/bin/claude",
) )
cmd = transport._build_command() cmd = transport._build_command()
@ -311,33 +395,46 @@ class TestSubprocessCLITransport:
"MY_TEST_VAR": test_value, "MY_TEST_VAR": test_value,
} }
options = ClaudeCodeOptions(env=custom_env) options = make_options(env=custom_env)
# Mock the subprocess to capture the env argument # Mock the subprocess to capture the env argument
with patch( with patch(
"anyio.open_process", new_callable=AsyncMock "anyio.open_process", new_callable=AsyncMock
) as mock_open_process: ) as mock_open_process:
# Mock version check process
mock_version_process = MagicMock()
mock_version_process.stdout = MagicMock()
mock_version_process.stdout.receive = AsyncMock(
return_value=b"2.0.0 (Claude Code)"
)
mock_version_process.terminate = MagicMock()
mock_version_process.wait = AsyncMock()
# Mock main process
mock_process = MagicMock() mock_process = MagicMock()
mock_process.stdout = MagicMock() mock_process.stdout = MagicMock()
mock_stdin = MagicMock() mock_stdin = MagicMock()
mock_stdin.aclose = AsyncMock() # Add async aclose method mock_stdin.aclose = AsyncMock() # Add async aclose method
mock_process.stdin = mock_stdin mock_process.stdin = mock_stdin
mock_process.returncode = None mock_process.returncode = None
mock_open_process.return_value = mock_process
# Return version process first, then main process
mock_open_process.side_effect = [mock_version_process, mock_process]
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=options, options=options,
cli_path="/usr/bin/claude",
) )
await transport.connect() await transport.connect()
# Verify open_process was called with correct env vars # Verify open_process was called twice (version check + main process)
mock_open_process.assert_called_once() assert mock_open_process.call_count == 2
call_kwargs = mock_open_process.call_args.kwargs
assert "env" in call_kwargs # Check the second call (main process) for env vars
env_passed = call_kwargs["env"] second_call_kwargs = mock_open_process.call_args_list[1].kwargs
assert "env" in second_call_kwargs
env_passed = second_call_kwargs["env"]
# Check that custom env var was passed # Check that custom env var was passed
assert env_passed["MY_TEST_VAR"] == test_value assert env_passed["MY_TEST_VAR"] == test_value
@ -358,35 +455,374 @@ class TestSubprocessCLITransport:
async def _test(): async def _test():
custom_user = "claude" custom_user = "claude"
options = ClaudeCodeOptions(user=custom_user) options = make_options(user=custom_user)
# Mock the subprocess to capture the env argument # Mock the subprocess to capture the env argument
with patch( with patch(
"anyio.open_process", new_callable=AsyncMock "anyio.open_process", new_callable=AsyncMock
) as mock_open_process: ) as mock_open_process:
# Mock version check process
mock_version_process = MagicMock()
mock_version_process.stdout = MagicMock()
mock_version_process.stdout.receive = AsyncMock(
return_value=b"2.0.0 (Claude Code)"
)
mock_version_process.terminate = MagicMock()
mock_version_process.wait = AsyncMock()
# Mock main process
mock_process = MagicMock() mock_process = MagicMock()
mock_process.stdout = MagicMock() mock_process.stdout = MagicMock()
mock_stdin = MagicMock() mock_stdin = MagicMock()
mock_stdin.aclose = AsyncMock() # Add async aclose method mock_stdin.aclose = AsyncMock() # Add async aclose method
mock_process.stdin = mock_stdin mock_process.stdin = mock_stdin
mock_process.returncode = None mock_process.returncode = None
mock_open_process.return_value = mock_process
# Return version process first, then main process
mock_open_process.side_effect = [mock_version_process, mock_process]
transport = SubprocessCLITransport( transport = SubprocessCLITransport(
prompt="test", prompt="test",
options=options, options=options,
cli_path="/usr/bin/claude",
) )
await transport.connect() await transport.connect()
# Verify open_process was called with correct user # Verify open_process was called twice (version check + main process)
mock_open_process.assert_called_once() assert mock_open_process.call_count == 2
call_kwargs = mock_open_process.call_args.kwargs
assert "user" in call_kwargs # Check the second call (main process) for user
user_passed = call_kwargs["user"] second_call_kwargs = mock_open_process.call_args_list[1].kwargs
assert "user" in second_call_kwargs
user_passed = second_call_kwargs["user"]
# Check that user was passed # Check that user was passed
assert user_passed == "claude" assert user_passed == "claude"
anyio.run(_test) anyio.run(_test)
def test_build_command_with_sandbox_only(self):
"""Test building CLI command with sandbox settings (no existing settings)."""
import json
from claude_agent_sdk import SandboxSettings
sandbox: SandboxSettings = {
"enabled": True,
"autoAllowBashIfSandboxed": True,
"network": {
"allowLocalBinding": True,
"allowUnixSockets": ["/var/run/docker.sock"],
},
}
transport = SubprocessCLITransport(
prompt="test",
options=make_options(sandbox=sandbox),
)
cmd = transport._build_command()
# Should have --settings with sandbox merged in
assert "--settings" in cmd
settings_idx = cmd.index("--settings")
settings_value = cmd[settings_idx + 1]
# Parse and verify
parsed = json.loads(settings_value)
assert "sandbox" in parsed
assert parsed["sandbox"]["enabled"] is True
assert parsed["sandbox"]["autoAllowBashIfSandboxed"] is True
assert parsed["sandbox"]["network"]["allowLocalBinding"] is True
assert parsed["sandbox"]["network"]["allowUnixSockets"] == [
"/var/run/docker.sock"
]
def test_build_command_with_sandbox_and_settings_json(self):
"""Test building CLI command with sandbox merged into existing settings JSON."""
import json
from claude_agent_sdk import SandboxSettings
# Existing settings as JSON string
existing_settings = (
'{"permissions": {"allow": ["Bash(ls:*)"]}, "verbose": true}'
)
sandbox: SandboxSettings = {
"enabled": True,
"excludedCommands": ["git", "docker"],
}
transport = SubprocessCLITransport(
prompt="test",
options=make_options(settings=existing_settings, sandbox=sandbox),
)
cmd = transport._build_command()
# Should have merged settings
assert "--settings" in cmd
settings_idx = cmd.index("--settings")
settings_value = cmd[settings_idx + 1]
parsed = json.loads(settings_value)
# Original settings should be preserved
assert parsed["permissions"] == {"allow": ["Bash(ls:*)"]}
assert parsed["verbose"] is True
# Sandbox should be merged in
assert "sandbox" in parsed
assert parsed["sandbox"]["enabled"] is True
assert parsed["sandbox"]["excludedCommands"] == ["git", "docker"]
def test_build_command_with_settings_file_and_no_sandbox(self):
"""Test that settings file path is passed through when no sandbox."""
transport = SubprocessCLITransport(
prompt="test",
options=make_options(settings="/path/to/settings.json"),
)
cmd = transport._build_command()
# Should pass path directly, not parse it
assert "--settings" in cmd
settings_idx = cmd.index("--settings")
assert cmd[settings_idx + 1] == "/path/to/settings.json"
def test_build_command_sandbox_minimal(self):
"""Test sandbox with minimal configuration."""
import json
from claude_agent_sdk import SandboxSettings
sandbox: SandboxSettings = {"enabled": True}
transport = SubprocessCLITransport(
prompt="test",
options=make_options(sandbox=sandbox),
)
cmd = transport._build_command()
assert "--settings" in cmd
settings_idx = cmd.index("--settings")
settings_value = cmd[settings_idx + 1]
parsed = json.loads(settings_value)
assert parsed == {"sandbox": {"enabled": True}}
def test_sandbox_network_config(self):
"""Test sandbox with full network configuration."""
import json
from claude_agent_sdk import SandboxSettings
sandbox: SandboxSettings = {
"enabled": True,
"network": {
"allowUnixSockets": ["/tmp/ssh-agent.sock"],
"allowAllUnixSockets": False,
"allowLocalBinding": True,
"httpProxyPort": 8080,
"socksProxyPort": 8081,
},
}
transport = SubprocessCLITransport(
prompt="test",
options=make_options(sandbox=sandbox),
)
cmd = transport._build_command()
settings_idx = cmd.index("--settings")
settings_value = cmd[settings_idx + 1]
parsed = json.loads(settings_value)
network = parsed["sandbox"]["network"]
assert network["allowUnixSockets"] == ["/tmp/ssh-agent.sock"]
assert network["allowAllUnixSockets"] is False
assert network["allowLocalBinding"] is True
assert network["httpProxyPort"] == 8080
assert network["socksProxyPort"] == 8081
def test_build_command_with_tools_array(self):
"""Test building CLI command with tools as array of tool names."""
transport = SubprocessCLITransport(
prompt="test",
options=make_options(tools=["Read", "Edit", "Bash"]),
)
cmd = transport._build_command()
assert "--tools" in cmd
tools_idx = cmd.index("--tools")
assert cmd[tools_idx + 1] == "Read,Edit,Bash"
def test_build_command_with_tools_empty_array(self):
"""Test building CLI command with tools as empty array (disables all tools)."""
transport = SubprocessCLITransport(
prompt="test",
options=make_options(tools=[]),
)
cmd = transport._build_command()
assert "--tools" in cmd
tools_idx = cmd.index("--tools")
assert cmd[tools_idx + 1] == ""
def test_build_command_with_tools_preset(self):
"""Test building CLI command with tools preset."""
transport = SubprocessCLITransport(
prompt="test",
options=make_options(tools={"type": "preset", "preset": "claude_code"}),
)
cmd = transport._build_command()
assert "--tools" in cmd
tools_idx = cmd.index("--tools")
assert cmd[tools_idx + 1] == "default"
def test_build_command_without_tools(self):
"""Test building CLI command without tools option (default None)."""
transport = SubprocessCLITransport(
prompt="test",
options=make_options(),
)
cmd = transport._build_command()
assert "--tools" not in cmd
def test_concurrent_writes_are_serialized(self):
"""Test that concurrent write() calls are serialized by the lock.
When parallel subagents invoke MCP tools, they trigger concurrent write()
calls. Without the _write_lock, trio raises BusyResourceError.
Uses a real subprocess with the same stream setup as production:
process.stdin -> TextSendStream
"""
async def _test():
import sys
from subprocess import PIPE
from anyio.streams.text import TextSendStream
# Create a real subprocess that consumes stdin (cross-platform)
process = await anyio.open_process(
[sys.executable, "-c", "import sys; sys.stdin.read()"],
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
)
try:
transport = SubprocessCLITransport(
prompt="test",
options=ClaudeAgentOptions(cli_path="/usr/bin/claude"),
)
# Same setup as production: TextSendStream wrapping process.stdin
transport._ready = True
transport._process = MagicMock(returncode=None)
transport._stdin_stream = TextSendStream(process.stdin)
# Spawn concurrent writes - the lock should serialize them
num_writes = 10
errors: list[Exception] = []
async def do_write(i: int):
try:
await transport.write(f'{{"msg": {i}}}\n')
except Exception as e:
errors.append(e)
async with anyio.create_task_group() as tg:
for i in range(num_writes):
tg.start_soon(do_write, i)
# All writes should succeed - the lock serializes them
assert len(errors) == 0, f"Got errors: {errors}"
finally:
process.terminate()
await process.wait()
anyio.run(_test, backend="trio")
def test_concurrent_writes_fail_without_lock(self):
"""Verify that without the lock, concurrent writes cause BusyResourceError.
Uses a real subprocess with the same stream setup as production.
"""
async def _test():
import sys
from contextlib import asynccontextmanager
from subprocess import PIPE
from anyio.streams.text import TextSendStream
# Create a real subprocess that consumes stdin (cross-platform)
process = await anyio.open_process(
[sys.executable, "-c", "import sys; sys.stdin.read()"],
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
)
try:
transport = SubprocessCLITransport(
prompt="test",
options=ClaudeAgentOptions(cli_path="/usr/bin/claude"),
)
# Same setup as production
transport._ready = True
transport._process = MagicMock(returncode=None)
transport._stdin_stream = TextSendStream(process.stdin)
# Replace lock with no-op to trigger the race condition
class NoOpLock:
@asynccontextmanager
async def __call__(self):
yield
async def __aenter__(self):
return self
async def __aexit__(self, *args):
pass
transport._write_lock = NoOpLock()
# Spawn concurrent writes - should fail without lock
num_writes = 10
errors: list[Exception] = []
async def do_write(i: int):
try:
await transport.write(f'{{"msg": {i}}}\n')
except Exception as e:
errors.append(e)
async with anyio.create_task_group() as tg:
for i in range(num_writes):
tg.start_soon(do_write, i)
# Should have gotten errors due to concurrent access
assert len(errors) > 0, (
"Expected errors from concurrent access, but got none"
)
# Check that at least one error mentions the concurrent access
error_strs = [str(e) for e in errors]
assert any("another task" in s for s in error_strs), (
f"Expected 'another task' error, got: {error_strs}"
)
finally:
process.terminate()
await process.wait()
anyio.run(_test, backend="trio")

View file

@ -1,11 +1,11 @@
"""Tests for Claude SDK type definitions.""" """Tests for Claude SDK type definitions."""
from claude_code_sdk import ( from claude_agent_sdk import (
AssistantMessage, AssistantMessage,
ClaudeCodeOptions, ClaudeAgentOptions,
ResultMessage, ResultMessage,
) )
from claude_code_sdk.types import ( from claude_agent_sdk.types import (
TextBlock, TextBlock,
ThinkingBlock, ThinkingBlock,
ToolResultBlock, ToolResultBlock,
@ -78,7 +78,7 @@ class TestOptions:
def test_default_options(self): def test_default_options(self):
"""Test Options with default values.""" """Test Options with default values."""
options = ClaudeCodeOptions() options = ClaudeAgentOptions()
assert options.allowed_tools == [] assert options.allowed_tools == []
assert options.system_prompt is None assert options.system_prompt is None
assert options.permission_mode is None assert options.permission_mode is None
@ -87,7 +87,7 @@ class TestOptions:
def test_claude_code_options_with_tools(self): def test_claude_code_options_with_tools(self):
"""Test Options with built-in tools.""" """Test Options with built-in tools."""
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Edit"], disallowed_tools=["Bash"] allowed_tools=["Read", "Write", "Edit"], disallowed_tools=["Bash"]
) )
assert options.allowed_tools == ["Read", "Write", "Edit"] assert options.allowed_tools == ["Read", "Write", "Edit"]
@ -95,37 +95,57 @@ class TestOptions:
def test_claude_code_options_with_permission_mode(self): def test_claude_code_options_with_permission_mode(self):
"""Test Options with permission mode.""" """Test Options with permission mode."""
options = ClaudeCodeOptions(permission_mode="bypassPermissions") options = ClaudeAgentOptions(permission_mode="bypassPermissions")
assert options.permission_mode == "bypassPermissions" assert options.permission_mode == "bypassPermissions"
options_plan = ClaudeCodeOptions(permission_mode="plan") options_plan = ClaudeAgentOptions(permission_mode="plan")
assert options_plan.permission_mode == "plan" assert options_plan.permission_mode == "plan"
options_default = ClaudeCodeOptions(permission_mode="default") options_default = ClaudeAgentOptions(permission_mode="default")
assert options_default.permission_mode == "default" assert options_default.permission_mode == "default"
options_accept = ClaudeCodeOptions(permission_mode="acceptEdits") options_accept = ClaudeAgentOptions(permission_mode="acceptEdits")
assert options_accept.permission_mode == "acceptEdits" assert options_accept.permission_mode == "acceptEdits"
def test_claude_code_options_with_system_prompt(self): def test_claude_code_options_with_system_prompt_string(self):
"""Test Options with system prompt.""" """Test Options with system prompt as string."""
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
system_prompt="You are a helpful assistant.", system_prompt="You are a helpful assistant.",
append_system_prompt="Be concise.",
) )
assert options.system_prompt == "You are a helpful assistant." assert options.system_prompt == "You are a helpful assistant."
assert options.append_system_prompt == "Be concise."
def test_claude_code_options_with_system_prompt_preset(self):
"""Test Options with system prompt preset."""
options = ClaudeAgentOptions(
system_prompt={"type": "preset", "preset": "claude_code"},
)
assert options.system_prompt == {"type": "preset", "preset": "claude_code"}
def test_claude_code_options_with_system_prompt_preset_and_append(self):
"""Test Options with system prompt preset and append."""
options = ClaudeAgentOptions(
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": "Be concise.",
},
)
assert options.system_prompt == {
"type": "preset",
"preset": "claude_code",
"append": "Be concise.",
}
def test_claude_code_options_with_session_continuation(self): def test_claude_code_options_with_session_continuation(self):
"""Test Options with session continuation.""" """Test Options with session continuation."""
options = ClaudeCodeOptions(continue_conversation=True, resume="session-123") options = ClaudeAgentOptions(continue_conversation=True, resume="session-123")
assert options.continue_conversation is True assert options.continue_conversation is True
assert options.resume == "session-123" assert options.resume == "session-123"
def test_claude_code_options_with_model_specification(self): def test_claude_code_options_with_model_specification(self):
"""Test Options with model specification.""" """Test Options with model specification."""
options = ClaudeCodeOptions( options = ClaudeAgentOptions(
model="claude-3-5-sonnet-20241022", permission_prompt_tool_name="CustomTool" model="claude-sonnet-4-5", permission_prompt_tool_name="CustomTool"
) )
assert options.model == "claude-3-5-sonnet-20241022" assert options.model == "claude-sonnet-4-5"
assert options.permission_prompt_tool_name == "CustomTool" assert options.permission_prompt_tool_name == "CustomTool"