Claude Agent SDK Java
Unofficial Claude Agent Java SDK
Ask AI about Claude Agent SDK Java
Powered by Claude Β· Grounded in docs
I know everything about Claude Agent SDK Java. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
Claude Agent SDK for Java
Java SDK for Claude Agent. This SDK provides a comprehensive Java API for interacting with Claude Code, enabling you to build AI-powered applications with Claude's capabilities.
Requirements
- Java 25 (uses virtual threads and sealed interfaces)
- Maven 3.6+
Note: The Claude Code CLI must be installed separately:
curl -fsSL https://claude.ai/install.sh | bash
Or specify a custom path:
ClaudeAgentOptions.builder()
.cliPath(Path.of("/path/to/claude"))
.build();
Installation
Maven
<dependency>
<groupId>in.vidyalai</groupId>
<artifactId>claude-agent-sdk-java</artifactId>
<version>0.1.2</version>
</dependency>
Add the GitHub Packages repository to your pom.xml:
<repositories>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/vidyalai-in/claude-agent-sdk-java</url>
</repository>
</repositories>
Gradle
implementation("in.vidyalai:claude-agent-sdk-java:0.1.2")
Add the GitHub Packages repository to your build.gradle.kts:
repositories {
maven {
url = uri("https://maven.pkg.github.com/vidyalai-in/claude-agent-sdk-java")
credentials {
username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME")
password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN")
}
}
}
Authentication for GitHub Packages
To use this library from GitHub Packages, you need to authenticate with GitHub.
Maven Authentication
Add this to your ~/.m2/settings.xml:
<settings>
<servers>
<server>
<id>github</id>
<username>YOUR_GITHUB_USERNAME</username>
<password>YOUR_GITHUB_PERSONAL_ACCESS_TOKEN</password>
</server>
</servers>
</settings>
Generate a personal access token with read:packages scope at: https://github.com/settings/tokens
Gradle Authentication
Create or update ~/.gradle/gradle.properties:
gpr.user=YOUR_GITHUB_USERNAME
gpr.key=YOUR_GITHUB_PERSONAL_ACCESS_TOKEN
Quick Start
import in.vidyalai.claude.sdk.ClaudeSDK;
import in.vidyalai.claude.sdk.types.Message;
import in.vidyalai.claude.sdk.types.AssistantMessage;
public class QuickStart {
public static void main(String[] args) {
// Simple one-shot query
for (Message message : ClaudeSDK.query("What is 2 + 2?")) {
if (message instanceof AssistantMessage assistant) {
System.out.println(assistant.getTextContent());
}
}
}
}
Basic Usage: ClaudeSDK.query()
ClaudeSDK.query() is for simple, one-shot queries. It returns a List<Message> with all response messages.
import in.vidyalai.claude.sdk.ClaudeSDK;
import in.vidyalai.claude.sdk.ClaudeAgentOptions;
import in.vidyalai.claude.sdk.types.*;
// Simple query
List<Message> messages = ClaudeSDK.query("Hello Claude");
for (Message msg : messages) {
if (msg instanceof AssistantMessage assistant) {
for (ContentBlock block : assistant.content()) {
if (block instanceof TextBlock text) {
System.out.println(text.text());
}
}
}
}
// With options
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.systemPrompt("You are a helpful assistant")
.maxTurns(1)
.build();
List<Message> response = ClaudeSDK.query("Tell me a joke", options);
Convenience Methods
// Get just the text response
String text = ClaudeSDK.queryForText("What is the capital of France?");
System.out.println(text); // "Paris"
// Get just the result message
ResultMessage result = ClaudeSDK.queryForResult("Do something", options);
System.out.println("Cost: $" + result.totalCostUsd());
Using Tools
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.allowedTools(List.of("Read", "Write", "Bash"))
.permissionMode(PermissionMode.ACCEPT_EDITS) // Auto-accept file edits
.build();
List<Message> messages = ClaudeSDK.query("Create a hello.py file", options);
for (Message msg : messages) {
if (msg instanceof AssistantMessage assistant) {
if (assistant.hasToolUse()) {
for (ContentBlock block : assistant.content()) {
if (block instanceof ToolUseBlock toolUse) {
System.out.println("Tool: " + toolUse.name());
System.out.println("Input: " + toolUse.input());
}
}
}
}
}
Working Directory
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.cwd(Path.of("/path/to/project"))
.build();
Streaming Input (Multiple Messages)
// Send multiple messages in sequence
var messages = List.of(
Map.of("type", "user", "session_id", "default",
"message", Map.of("role", "user", "content", "First message")),
Map.of("type", "user", "session_id", "default",
"message", Map.of("role", "user", "content", "Follow-up question"))
);
List<Message> responses = ClaudeSDK.query(messages.iterator(), options);
ClaudeSDKClient
ClaudeSDKClient supports bidirectional, interactive conversations with Claude Code. Unlike query(), it enables multi-turn conversations, custom tools, hooks, and real-time interaction.
Basic Usage
import in.vidyalai.claude.sdk.ClaudeSDKClient;
import in.vidyalai.claude.sdk.ClaudeAgentOptions;
import in.vidyalai.claude.sdk.types.*;
// Using try-with-resources (recommended)
try (var client = ClaudeSDK.createClient()) {
client.connect("Hello, Claude!");
for (Message msg : client.receiveResponse()) {
if (msg instanceof AssistantMessage assistant) {
System.out.println(assistant.getTextContent());
}
}
}
// Multi-turn conversation
try (var client = ClaudeSDK.createClient()) {
// First turn
client.connect("What is machine learning?");
for (Message msg : client.receiveResponse()) {
System.out.println(msg);
}
// Follow-up
client.sendMessage("Can you give me an example?");
for (Message msg : client.receiveResponse()) {
System.out.println(msg);
}
}
Manual Connection Management
ClaudeSDKClient client = new ClaudeSDKClient();
try {
client.connect("Start a conversation");
// Receive messages
for (Message msg : client.receiveResponse()) {
process(msg);
}
// Send follow-up
client.sendMessage("Continue...");
for (Message msg : client.receiveResponse()) {
process(msg);
}
} finally {
client.disconnect();
}
Interrupt Execution
try (var client = ClaudeSDK.createClient()) {
client.connect("Do a long task");
// In another thread or after some condition
client.interrupt(); // Sends interrupt signal
}
Dynamic Model Switching
try (var client = ClaudeSDK.createClient()) {
client.connect("Start with default model");
// Switch to a different model mid-conversation
client.setModel("claude-sonnet-4-5");
client.sendMessage("Continue with new model");
}
Permission Mode Changes
try (var client = ClaudeSDK.createClient()) {
client.connect("Start in default mode");
// Change permission mode during conversation
client.setPermissionMode(PermissionMode.BYPASS_PERMISSIONS);
client.sendMessage("Now run dangerous commands");
}
MCP Server Status
try (var client = ClaudeSDK.createClient(options)) {
client.connect();
// Query MCP server connection status
Map<String, Object> status = client.getMcpStatus();
List<?> servers = (List<?>) status.get("mcpServers");
for (Object server : servers) {
Map<?, ?> serverInfo = (Map<?, ?>) server;
System.out.println("Server: " + serverInfo.get("name") +
" Status: " + serverInfo.get("status"));
}
}
SDK Utilities
// Get SDK version
String version = ClaudeSDK.getVersion();
System.out.println("Claude SDK Version: " + version);
// Check if client is connected
try (var client = ClaudeSDK.createClient()) {
System.out.println("Connected: " + client.isConnected()); // false
client.connect();
System.out.println("Connected: " + client.isConnected()); // true
}
Custom Tools (SDK MCP Servers)
Create in-process MCP servers that run directly within your Java application.
Using @Tool Annotation
import in.vidyalai.claude.sdk.mcp.Tool;
import in.vidyalai.claude.sdk.mcp.ToolResult;
public class MyTools {
@Tool(name = "greet", description = "Greet a user by name")
public CompletableFuture<ToolResult> greet(Map<String, Object> args) {
String name = (String) args.get("name");
return CompletableFuture.completedFuture(
ToolResult.text("Hello, " + name + "!")
);
}
@Tool(name = "calculate", description = "Perform a calculation")
public CompletableFuture<ToolResult> calculate(Map<String, Object> args) {
int a = ((Number) args.get("a")).intValue();
int b = ((Number) args.get("b")).intValue();
String op = (String) args.get("operation");
int result = switch (op) {
case "add" -> a + b;
case "subtract" -> a - b;
case "multiply" -> a * b;
case "divide" -> a / b;
default -> throw new IllegalArgumentException("Unknown operation: " + op);
};
return CompletableFuture.completedFuture(
ToolResult.text(String.valueOf(result))
);
}
}
// Create SDK MCP server from annotated methods
McpSdkServerConfig serverConfig = ClaudeSDK.createSdkMcpServer(
"my-tools",
new MyTools()
);
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.mcpServers(Map.of("tools", serverConfig))
.allowedTools(List.of("mcp__tools__greet", "mcp__tools__calculate"))
.build();
try (var client = ClaudeSDK.createClient(options)) {
client.connect("Greet Alice and calculate 5 + 3");
for (Message msg : client.receiveResponse()) {
System.out.println(msg);
}
}
Using SdkMcpTool Directly
import in.vidyalai.claude.sdk.mcp.*;
// Create tools programmatically
SdkMcpTool<Map<String, Object>> greetTool = SdkMcpTool.create(
"greet",
"Greet a user",
Map.of(
"type", "object",
"properties", Map.of(
"name", Map.of("type", "string", "description", "Name to greet")
),
"required", List.of("name")
),
args -> {
String name = (String) args.get("name");
return CompletableFuture.completedFuture(
ToolResult.text("Hello, " + name + "!")
);
}
);
// Create server
SdkMcpServer server = SdkMcpServer.create("my-server", "1.0.0", List.of(greetTool));
McpSdkServerConfig config = server.toConfig();
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.mcpServers(Map.of("my-server", config))
.allowedTools(List.of("mcp__my-server__greet"))
.build();
Tool Result Types
// Text result
ToolResult.text("Operation completed successfully")
// Error result
ToolResult.error("File not found: /path/to/file")
// Image result (base64 encoded)
ToolResult.image(base64Data, "image/png")
Benefits Over External MCP Servers
- No subprocess management - Runs in the same JVM as your application
- Better performance - No IPC overhead for tool calls
- Simpler deployment - Single Java process instead of multiple
- Easier debugging - All code runs in the same process
- Type safety - Direct Java method calls
Hooks
Hooks are callbacks that Claude Code invokes at specific points in the agent loop. They enable deterministic processing and automated feedback.
Hook Events
| Event | Description |
|---|---|
PreToolUse | Before a tool is executed |
PostToolUse | After a tool completes |
UserPromptSubmit | When user submits a prompt |
Stop | When session stops |
SubagentStop | When a subagent stops |
PreCompact | Before context compaction |
Example: Blocking Dangerous Commands
import in.vidyalai.claude.sdk.types.*;
import java.util.concurrent.CompletableFuture;
HookMatcher.HookCallback checkBashCommand = (input, context) -> {
if (input instanceof PreToolUseHookInput preToolUse) {
if ("Bash".equals(preToolUse.toolName())) {
String command = (String) preToolUse.toolInput().get("command");
// Block dangerous patterns
List<String> blockedPatterns = List.of("rm -rf", "sudo", "chmod 777");
for (String pattern : blockedPatterns) {
if (command.contains(pattern)) {
return CompletableFuture.completedFuture(
HookOutput.builder()
.hookSpecificOutput(
HookSpecificOutput.preToolUse()
.permissionDecision("deny")
.permissionDecisionReason("Command contains blocked pattern: " + pattern)
.build()
)
.build()
);
}
}
}
}
return CompletableFuture.completedFuture(HookOutput.empty());
};
HookMatcher bashMatcher = new HookMatcher("Bash", List.of(checkBashCommand));
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.allowedTools(List.of("Bash"))
.hooks(Map.of(HookEvent.PRE_TOOL_USE, List.of(bashMatcher)))
.build();
try (var client = ClaudeSDK.createClient(options)) {
// This will be blocked
client.connect("Run: rm -rf /");
for (Message msg : client.receiveResponse()) {
System.out.println(msg);
}
}
Example: Logging All Tool Uses
HookMatcher.HookCallback logToolUse = (input, context) -> {
if (input instanceof PostToolUseHookInput postToolUse) {
System.out.printf("Tool '%s' completed with response: %s%n",
postToolUse.toolName(),
postToolUse.toolResponse());
}
return CompletableFuture.completedFuture(HookOutput.empty());
};
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.hooks(Map.of(
HookEvent.POST_TOOL_USE, List.of(new HookMatcher("*", List.of(logToolUse)))
))
.build();
Permission Callbacks
Control tool execution with custom permission logic.
import in.vidyalai.claude.sdk.types.*;
import java.util.concurrent.CompletableFuture;
ClaudeAgentOptions.CanUseTool permissionCallback = (toolName, input, context) -> {
// Check tool name
if ("Bash".equals(toolName)) {
String command = (String) input.get("command");
// Allow safe commands
if (command.startsWith("ls") || command.startsWith("cat") || command.startsWith("echo")) {
return CompletableFuture.completedFuture(new PermissionResultAllow());
}
// Deny dangerous commands
return CompletableFuture.completedFuture(
new PermissionResultDeny("Bash command not allowed: " + command, false)
);
}
// Allow all other tools
return CompletableFuture.completedFuture(new PermissionResultAllow());
};
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.canUseTool(permissionCallback)
.allowedTools(List.of("Bash", "Read", "Write"))
.build();
Modifying Tool Input
ClaudeAgentOptions.CanUseTool sanitizeInput = (toolName, input, context) -> {
if ("Bash".equals(toolName)) {
// Modify the input
Map<String, Object> modifiedInput = new HashMap<>(input);
modifiedInput.put("timeout", 30); // Add timeout
return CompletableFuture.completedFuture(
new PermissionResultAllow(modifiedInput, null)
);
}
return CompletableFuture.completedFuture(new PermissionResultAllow());
};
Permission Updates
ClaudeAgentOptions.CanUseTool upgradePermissions = (toolName, input, context) -> {
// Grant bypass permissions for this session
List<PermissionUpdate> updates = List.of(
PermissionUpdate.setMode(
PermissionMode.BYPASS_PERMISSIONS,
PermissionUpdateDestination.SESSION
)
);
return CompletableFuture.completedFuture(
new PermissionResultAllow(null, updates)
);
};
Configuration Options
ClaudeAgentOptions
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
// Model configuration
.model("claude-sonnet-4-5")
.fallbackModel("claude-haiku-3-5")
// Thinking configuration (new in v0.1.36 - takes precedence over maxThinkingTokens)
.thinking(new ThinkingConfigEnabled(10_000)) // Enable with 10k token budget
// Or: .thinking(new ThinkingConfigAdaptive()) // Adaptive (32k default)
// Or: .thinking(new ThinkingConfigDisabled()) // Disable thinking
.effort("medium") // Thinking depth: "low", "medium", "high", "max"
// Legacy thinking config (deprecated - use .thinking() instead)
.maxThinkingTokens(8000)
// Tool configuration
.tools(List.of("Bash", "Read", "Write", "Edit"))
.allowedTools(List.of("Read"))
.disallowedTools(List.of("Execute"))
// Permission configuration
.permissionMode(PermissionMode.ACCEPT_EDITS)
.canUseTool(permissionCallback)
// System prompt
.systemPrompt("You are a helpful coding assistant")
// Or use preset
.systemPrompt(SystemPromptPreset.claudeCode("Be concise."))
// Session configuration
.continueConversation(true)
.resume("session-id")
.forkSession(false)
// Limits
.maxTurns(10)
.maxBudgetUsd(1.0)
// Working directory
.cwd(Path.of("/path/to/project"))
.addDirs(List.of(Path.of("/other/path")))
// MCP servers
.mcpServers(Map.of("server", serverConfig))
// Hooks
.hooks(Map.of(HookEvent.PRE_TOOL_USE, List.of(matcher)))
// Sandbox
.sandbox(new SandboxSettings(true))
// Environment
.env(Map.of("MY_VAR", "value"))
// Beta features
.betas(List.of("context-1m-2025-08-07"))
// Streaming
.includePartialMessages(true)
// File checkpointing
.enableFileCheckpointing(true)
// Output format (structured output)
.outputFormat(Map.of(
"type", "json_schema",
"schema", schemaMap
))
.build();
Permission Modes
PermissionMode.DEFAULT // CLI prompts for dangerous tools
PermissionMode.ACCEPT_EDITS // Auto-accept file edits
PermissionMode.PLAN // Show plans before execution
PermissionMode.BYPASS_PERMISSIONS // Allow all tools (use with caution)
Thinking Configuration
Control extended thinking behavior with ThinkingConfig types (added in v0.1.36):
import in.vidyalai.claude.sdk.types.config.*;
// Adaptive thinking - uses 32,000 token budget by default
ThinkingConfig adaptive = new ThinkingConfigAdaptive();
// Enabled thinking - specify exact token budget
ThinkingConfig enabled = new ThinkingConfigEnabled(10_000);
// Disabled thinking - no extended thinking
ThinkingConfig disabled = new ThinkingConfigDisabled();
// Use with ClaudeAgentOptions
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.thinking(enabled)
.effort("medium") // Control thinking depth: "low", "medium", "high", "max"
.build();
Note: The thinking field takes precedence over the deprecated maxThinkingTokens field.
MCP Server Configurations
// Stdio server (subprocess)
StdioMcpServerConfig stdio = new StdioMcpServerConfig(
"node",
List.of("server.js", "--port", "3000"),
Map.of("NODE_ENV", "production")
);
// SSE server
SseMcpServerConfig sse = new SseMcpServerConfig(
"https://api.example.com/sse",
Map.of("Authorization", "Bearer token")
);
// HTTP server
HttpMcpServerConfig http = new HttpMcpServerConfig(
"https://api.example.com/mcp",
Map.of("X-API-Key", "key123")
);
// SDK server (in-process)
McpSdkServerConfig sdk = ClaudeSDK.createSdkMcpServer("name", toolInstance);
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.mcpServers(Map.of(
"stdio-server", stdio,
"sse-server", sse,
"http-server", http,
"sdk-server", sdk
))
.build();
Sandbox Configuration
SandboxSettings sandbox = new SandboxSettings(
true, // enabled
true, // autoAllowBashIfSandboxed
List.of("git", "docker"), // excludedCommands
Path.of("/sandbox"), // sandboxDir
new SandboxNetworkConfig(
List.of("/tmp/ssh-agent.sock"), // allowUnixSockets
false, // allowAllUnixSockets
true, // allowLocalBinding
8080, // httpProxyPort
8081 // socksProxyPort
),
new SandboxIgnoreViolations(
List.of("/tmp"), // file paths to ignore
List.of("localhost") // network hosts to ignore
),
false // allowNestedSandbox
);
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.sandbox(sandbox)
.build();
Message Types
The SDK uses sealed interfaces for type-safe message handling with pattern matching.
import in.vidyalai.claude.sdk.types.*;
for (Message msg : messages) {
switch (msg) {
case UserMessage user -> {
System.out.println("User: " + user.contentAsString());
}
case AssistantMessage assistant -> {
System.out.println("Assistant: " + assistant.getTextContent());
if (assistant.hasToolUse()) {
System.out.println("(used tools)");
}
}
case SystemMessage system -> {
System.out.println("System: " + system.subtype());
}
case ResultMessage result -> {
System.out.println("Result: " + result.subtype());
System.out.println("Cost: $" + result.totalCostUsd());
System.out.println("Turns: " + result.numTurns());
}
case StreamEvent event -> {
System.out.println("Stream event: " + event.eventType());
}
}
}
Content Blocks
for (ContentBlock block : assistant.content()) {
switch (block) {
case TextBlock text -> System.out.println(text.text());
case ThinkingBlock thinking -> System.out.println("Thinking: " + thinking.thinking());
case ToolUseBlock toolUse -> {
System.out.println("Tool: " + toolUse.name());
System.out.println("Input: " + toolUse.input());
}
case ToolResultBlock result -> {
System.out.println("Result: " + result.content());
if (result.isError()) {
System.out.println("(error)");
}
}
}
}
Error Handling
import in.vidyalai.claude.sdk.exceptions.*;
try {
List<Message> messages = ClaudeSDK.query("Hello");
} catch (CLINotFoundException e) {
System.err.println("Claude Code CLI not found. Install with:");
System.err.println(" curl -fsSL https://claude.ai/install.sh | bash");
} catch (CLIConnectionException e) {
System.err.println("Failed to connect to CLI: " + e.getMessage());
} catch (ProcessException e) {
System.err.println("Process failed with exit code: " + e.getExitCode());
System.err.println("Stderr: " + e.getStderr());
} catch (CLIJSONDecodeException e) {
System.err.println("Failed to parse JSON response: " + e.getMessage());
System.err.println("Raw line: " + e.getLine());
} catch (MessageParseException e) {
System.err.println("Failed to parse message: " + e.getMessage());
System.err.println("Data: " + e.getData());
} catch (ClaudeSDKException e) {
System.err.println("SDK error: " + e.getMessage());
}
Exception Hierarchy
ClaudeSDKException (base)
βββ CLIConnectionException
β βββ CLINotFoundException
βββ ProcessException
βββ CLIJSONDecodeException
βββ MessageParseException
Streaming Events
Enable partial message streaming for real-time updates.
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.includePartialMessages(true)
.build();
try (var client = ClaudeSDK.createClient(options)) {
client.connect("Write a long story");
for (Message msg : client.receiveMessages()) {
if (msg instanceof StreamEvent event) {
// Process streaming delta
System.out.print(event.event().get("delta"));
} else if (msg instanceof AssistantMessage assistant) {
// Complete message
System.out.println("\n--- Complete ---");
System.out.println(assistant.getTextContent());
} else if (msg instanceof ResultMessage result) {
break; // Done
}
}
}
File Checkpointing
Track file changes and rewind to previous states.
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.enableFileCheckpointing(true)
.build();
try (var client = ClaudeSDK.createClient(options)) {
client.connect("Modify some files");
String checkpointId = null;
for (Message msg : client.receiveResponse()) {
if (msg instanceof UserMessage user && user.uuid() != null) {
checkpointId = user.uuid(); // Save checkpoint
}
}
// Later, rewind to checkpoint
if (checkpointId != null) {
client.rewindFiles(checkpointId);
}
}
Custom Agents
Define custom agents with specific capabilities.
AgentDefinition codeReviewer = new AgentDefinition(
"Code Review Agent",
"You are an expert code reviewer. Focus on security, performance, and best practices.",
List.of("Read", "Grep", "Glob"),
"sonnet"
);
AgentDefinition testWriter = new AgentDefinition(
"Test Writer Agent",
"You write comprehensive unit tests.",
List.of("Read", "Write", "Bash"),
"sonnet"
);
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.agents(Map.of(
"code-reviewer", codeReviewer,
"test-writer", testWriter
))
.build();
Examples
See the examples/ module for complete working examples:
QuickStart.java- Basic usageMultiTurnConversation.java- Interactive conversationsToolUsage.java- Using built-in toolsMcpServer.java- Creating custom MCP toolsAutoSchemaGeneration.java- Automatic schema generation for toolsHooks.java- Hook callbacks (including new hook events: Notification, SubagentStart, PermissionRequest)PermissionCallbacks.java- Custom permission logicStreamingEvents.java- Real-time streamingStructuredOutputExample.java- Structured output with JSON Schema validation (simple, nested, enum, with tools)DynamicControlExample.java- Dynamic control features (setPermissionMode, setModel, interrupt)ErrorHandling.java- Exception handlingAdvancedFeatures.java- Checkpointing, sandbox, structured outputToolsConfigurationExample.java- Tools configuration (array, preset, empty)MaxBudgetExample.java- Budget limiting and cost controlSettingSourcesExample.java- Settings sources (user, project, local)StderrCallbackExample.java- Capturing CLI stderr outputPluginsExample.java- Plugin system usageAgentsExample.java- Programmatic subagent definitionsFilesystemAgentsExample.java- Filesystem-based agent configurationLargeAgentsExample.java- Large agent definitions (260KB+) via initialize requestSystemPromptExample.java- Custom system prompt usageIncludePartialMessagesExample.java- Streaming with partial message updates
Running Examples
The examples module is a separate Maven module that depends on the published SDK from GitHub Packages.
Option 1: Run Examples from Root Directory (Recommended)
Build all modules and run an example:
# Build all modules (SDK + examples)
mvn clean package -DskipTests
# Run an example using Maven exec plugin
mvn exec:java -Dexec.mainClass="examples.QuickStart" -pl examples
# Run different examples
mvn exec:java -Dexec.mainClass="examples.MultiTurnConversation" -pl examples
mvn exec:java -Dexec.mainClass="examples.McpServer" -pl examples
Option 2: Run Examples from Examples Directory
# Navigate to examples directory
cd examples
# Build examples (downloads published SDK from GitHub Packages)
mvn clean package -DskipTests
# Run an example using Maven exec plugin
mvn exec:java -Dexec.mainClass="examples.QuickStart"
# Or use java -cp
java -cp target/classes:target/dependency/* examples.QuickStart
Option 3: Run Examples with Local Development SDK
To test examples against your local development version of the SDK (not the published version):
-
Install the SDK locally:
cd sdk mvn clean install -DskipTests cd .. -
Update
examples/pom.xmlto use the SNAPSHOT version:<dependency> <groupId>in.vidyalai</groupId> <artifactId>claude-agent-sdk-java</artifactId> <version>0.1.1-SNAPSHOT</version> </dependency> -
Run examples as described in Option 1 or 2.
Note: You can also make the examples module depend on the published version from GitHub Packages. You need GitHub authentication configured to download it (see "Authentication for GitHub Packages" section above).
Thread Safety
ClaudeSDKClientis not thread-safe. Use one client per thread or synchronize access.ClaudeSDK.query()methods create new connections and are safe to call from multiple threads.- Callbacks (hooks, permissions) may be called from different threads; ensure your callback implementations are thread-safe.
Concurrency Model: Python vs Java
The Java SDK uses a fundamentally different concurrency model than the Python SDK. Understanding these differences helps when porting code or comparing examples.
Python SDK: Async/Await Model
The Python SDK uses Python's async/await syntax with asyncio or trio:
# Python SDK - async/await with asyncio
async def example():
async with ClaudeSDKClient() as client:
await client.connect("Hello")
async for message in client.receive_response():
print(message)
# Python SDK - streaming with async iterables
async def message_stream():
yield {"type": "user", "message": {"role": "user", "content": "Hi"}}
yield {"type": "user", "message": {"role": "user", "content": "Bye"}}
await client.connect(message_stream())
Key Python Features:
async/awaitkeywords for non-blocking operationsasync forfor iterating over async iterablesasync withfor async context managers- Async iterables/generators (
async defwithyield) - Libraries: asyncio, trio
Java SDK: Synchronous + CompletableFuture Model
The Java SDK uses synchronous APIs with CompletableFuture for asynchronous operations:
// Java SDK - synchronous with try-with-resources
try (var client = ClaudeSDK.createClient()) {
client.connect("Hello");
for (Message message : client.receiveResponse()) {
System.out.println(message);
}
}
// Java SDK - streaming with Iterator
List<Map<String, Object>> messages = List.of(
Map.of("type", "user", "message", Map.of("role", "user", "content", "Hi")),
Map.of("type", "user", "message", Map.of("role", "user", "content", "Bye"))
);
client.query(messages.iterator());
Key Java Features:
- Synchronous iterators (
Iterator<Message>) instead of async iterables - Try-with-resources (
try (...)) instead of async context managers - CompletableFuture for async callbacks (hooks, permissions)
- Virtual threads (Java 21+) for efficient blocking I/O
- ExecutorService for background task management
Why Python Async Examples Don't Directly Translate
Some Python SDK examples don't have direct Java equivalents because they demonstrate async-specific patterns:
| Python Example | Why Not in Java | Java Equivalent |
|---|---|---|
streaming_mode_ipython.py | IPython-specific async REPL integration | Use Java REPL (jshell) with synchronous APIs |
streaming_mode_trio.py | Trio-specific concurrency library | Use standard Java concurrency (ExecutorService, virtual threads) |
test_connect_with_async_iterable | Async generator pattern | client.query(Iterator<Map>) with regular Iterator |
test_concurrent_send_receive | Async concurrent operations | Use Thread.startVirtualThread() or ExecutorService |
Concurrent Operations in Java
For operations that need true concurrency in Java:
// Concurrent send and receive using virtual threads
try (var client = ClaudeSDK.createClient()) {
client.connect();
// Receive in background thread
Thread receiveThread = Thread.startVirtualThread(() -> {
for (Message msg : client.receiveMessages()) {
if (msg instanceof ResultMessage) break;
System.out.println("Received: " + msg);
}
});
// Send from main thread
client.sendMessage("First message");
Thread.sleep(1000);
client.sendMessage("Second message");
receiveThread.join();
}
Performance Considerations
- Python: Async I/O is efficient for I/O-bound operations but requires explicit
awaitpoints - Java: Virtual threads (Project Loom) make blocking I/O as efficient as async without syntax changes
- Java: Synchronous APIs are simpler to use and debug than async code
- Python: Trio provides structured concurrency; Java achieves similar with try-with-resources and ExecutorService
Migration Guide: Python to Java
| Python Pattern | Java Equivalent |
|---|---|
async with client: | try (var client = ...) { |
await client.connect() | client.connect() (synchronous) |
async for msg in client.receive(): | for (Message msg : client.receiveResponse()) |
await asyncio.sleep(1) | Thread.sleep(1000) |
asyncio.create_task() | Thread.startVirtualThread(() -> ...) |
async def generator(): + yield | Iterator<T> implementation |
CompletableFuture.completed() | CompletableFuture.completedFuture() |
Bottom Line: The Java SDK prioritizes simplicity and uses synchronous APIs with virtual threads for efficient concurrency, while the Python SDK uses async/await for non-blocking operations. Both achieve similar functionality with their respective language idioms.
Best Practices
- Always close clients - Use try-with-resources or call
disconnect()in a finally block. - Handle errors gracefully - Catch specific exceptions for better error messages.
- Set appropriate timeouts - Use
maxTurnsandmaxBudgetUsdto limit execution. - Use permission callbacks for security - Don't rely solely on
permissionMode. - Prefer SDK MCP servers - They're faster and easier to debug than external processes.
Documentation
- Python SDK Feature Parity Analysis - Comprehensive comparison between Python and Java SDKs, including feature parity status, type system comparison, examples coverage, and implementation details.
