Skip to main content
Complete API reference for the Claude Agent SDK Java, including all classes, methods, and types.
This documentation follows the structure of the official Python SDK documentation.

Installation

Maven

<dependency>
    <groupId>org.springaicommunity</groupId>
    <artifactId>claude-code-sdk</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

Gradle

implementation 'org.springaicommunity:claude-code-sdk:1.0.0-SNAPSHOT'

Three-API Architecture

The Java SDK provides three ways to interact with Claude Code:

Quick Comparison

FeatureQueryClaudeSyncClientClaudeAsyncClient
StyleStatic methodsIterator-basedFlux/Mono (Reactor)
SessionNew each callReuses same sessionReuses same session
ConversationSingle exchangeMulti-turnMulti-turn
HooksNot supportedSupportedSupported
MCP ServersNot supportedSupportedSupported
Permission CallbacksNot supportedSupportedSupported
Use CaseCLI tools, scriptsTraditional appsNon-blocking apps
Both ClaudeSyncClient and ClaudeAsyncClient provide the same capabilities. They differ only in programming paradigm (blocking vs non-blocking).

When to Use Query

Best for:
  • One-off questions where you don’t need conversation history
  • Simple automation scripts
  • CLI tools and batch processing

When to Use ClaudeSyncClient

Best for:
  • Multi-turn conversations with context
  • Tool interception with hooks
  • MCP server integration
  • Standard Java iteration patterns

When to Use ClaudeAsyncClient

Best for:
  • Spring WebFlux applications
  • Server-Sent Events (SSE) streaming
  • Non-blocking I/O requirements
  • Reactive pipelines

Query - One-Shot API

Creates a new session for each interaction. Provides static methods for simple queries.

Methods

MethodReturn TypeDescription
text(prompt)StringSimple one-liner, returns text response
text(prompt, options)StringWith configuration options
execute(prompt)QueryResultFull result with metadata
execute(prompt, options)QueryResultWith configuration options
query(prompt)Iterable<Message>Iterate over messages
stream(prompt)Stream<Message>Java Stream over messages

Example - Simple query

import org.springaicommunity.claude.agent.sdk.Query;

// Simplest usage - one line
String answer = Query.text("What is 2+2?");
System.out.println(answer);  // "4"

Example - With options

import org.springaicommunity.claude.agent.sdk.Query;
import org.springaicommunity.claude.agent.sdk.QueryOptions;
import java.time.Duration;

QueryOptions options = QueryOptions.builder()
    .model("claude-sonnet-4-20250514")
    .appendSystemPrompt("Be concise")
    .timeout(Duration.ofMinutes(5))
    .build();

String response = Query.text("Explain Java", options);
System.out.println(response);

Example - Full result with metadata

import org.springaicommunity.claude.agent.sdk.Query;
import org.springaicommunity.claude.agent.sdk.types.QueryResult;

QueryResult result = Query.execute("Write a haiku about Java");

System.out.println(result.text().orElse(""));
System.out.println("Cost: $" + result.metadata().cost().calculateTotal());
System.out.println("Duration: " + result.metadata().getDuration().toMillis() + "ms");
System.out.println("Model: " + result.metadata().model());

Example - Streaming iteration

for (Message msg : Query.query("Explain recursion")) {
    if (msg instanceof AssistantMessage am) {
        am.getTextContent().ifPresent(System.out::print);
    }
}

// Or with Stream API
Query.stream("Explain recursion")
    .filter(msg -> msg instanceof AssistantMessage)
    .forEach(msg -> System.out.println(msg));

ClaudeClient - Factory

Factory class for creating sync and async clients.

Methods

MethodReturn TypeDescription
sync()ClaudeSyncClient.BuilderCreate blocking client builder
sync(options)ClaudeSyncClient.BuilderCreate builder with pre-configured CLIOptions
async()ClaudeAsyncClient.BuilderCreate reactive client builder
async(options)ClaudeAsyncClient.BuilderCreate builder with pre-configured CLIOptions

Example - Factory pattern

// Sync client with fluent builder
try (ClaudeSyncClient client = ClaudeClient.sync()
        .workingDirectory(Path.of("."))
        .model("claude-sonnet-4-20250514")
        .systemPrompt("You are helpful")
        .timeout(Duration.ofMinutes(5))
        .build()) {
    // Use client
}

// Async client
ClaudeAsyncClient client = ClaudeClient.async()
    .workingDirectory(Path.of("."))
    .permissionMode(PermissionMode.BYPASS_PERMISSIONS)
    .build();

ClaudeSyncClient - Blocking Client

Maintains a conversation session across multiple exchanges. Uses iterator-based message handling.

Methods

MethodDescription
connect(prompt)Start session with initial prompt
query(prompt)Send follow-up message
receiveResponse()Get iterator over response messages
interrupt()Stop current operation
close()End session and release resources

Example - Multi-turn conversation

import org.springaicommunity.claude.agent.sdk.ClaudeClient;
import org.springaicommunity.claude.agent.sdk.ClaudeSyncClient;
import org.springaicommunity.claude.agent.sdk.parsing.ParsedMessage;
import org.springaicommunity.claude.agent.sdk.types.AssistantMessage;
import java.util.Iterator;

try (ClaudeSyncClient client = ClaudeClient.sync()
        .workingDirectory(Path.of("."))
        .build()) {

    // First turn
    client.connect("My favorite color is blue. Remember this.");
    Iterator<ParsedMessage> response = client.receiveResponse();
    while (response.hasNext()) {
        ParsedMessage msg = response.next();
        if (msg.isRegularMessage() && msg.asMessage() instanceof AssistantMessage am) {
            am.getTextContent().ifPresent(System.out::println);
        }
    }

    // Second turn - Claude remembers context
    client.query("What is my favorite color?");
    response = client.receiveResponse();
    while (response.hasNext()) {
        ParsedMessage msg = response.next();
        if (msg.isRegularMessage() && msg.asMessage() instanceof AssistantMessage am) {
            am.getTextContent().ifPresent(System.out::println);  // "blue"
        }
    }
}

Example - With hooks

import org.springaicommunity.claude.agent.sdk.hooks.HookRegistry;
import org.springaicommunity.claude.agent.sdk.hooks.HookInput;
import org.springaicommunity.claude.agent.sdk.hooks.HookOutput;

HookRegistry hookRegistry = new HookRegistry();

// Block dangerous commands
hookRegistry.registerPreToolUse("Bash", input -> {
    if (input instanceof HookInput.PreToolUseInput preToolUse) {
        String cmd = preToolUse.getArgument("command", String.class).orElse("");
        if (cmd.contains("rm -rf")) {
            return HookOutput.block("Dangerous command blocked");
        }
    }
    return HookOutput.allow();
});

// Log all tool results
hookRegistry.registerPostToolUse(input -> {
    if (input instanceof HookInput.PostToolUseInput postToolUse) {
        System.out.println("Tool completed: " + postToolUse.toolName());
    }
    return HookOutput.allow();
});

try (ClaudeSyncClient client = ClaudeClient.sync()
        .workingDirectory(Path.of("."))
        .permissionMode(PermissionMode.DEFAULT)
        .hookRegistry(hookRegistry)
        .build()) {
    // Hooks intercept tool calls
}

Example - With MCP servers

import org.springaicommunity.claude.agent.sdk.mcp.McpServerConfig;

// External MCP server (subprocess)
McpServerConfig npmServer = McpServerConfig.command("npx")
    .args("-y", "@anthropic/mcp-server-filesystem")
    .env("HOME", System.getProperty("user.home"))
    .build();

// In-process SDK MCP server
McpServerConfig sdkServer = McpServerConfig.sdk(myMcpServer);

try (ClaudeSyncClient client = ClaudeClient.sync()
        .workingDirectory(Path.of("."))
        .mcpServer("filesystem", npmServer)
        .mcpServer("custom", sdkServer)
        .build()) {
    // MCP tools available to Claude
}

ClaudeAsyncClient - Reactive Client

Non-blocking reactive API using Project Reactor. Use when you need composable, non-blocking chains.

Methods

MethodReturn TypeDescription
connect(prompt)TurnSpecStart session with prompt
query(prompt)TurnSpecSend follow-up message
onMessage(handler)ClaudeAsyncClientRegister message handler
onResult(handler)ClaudeAsyncClientRegister result handler
close()Mono<Void>End session

TurnSpec Methods

MethodReturn TypeDescription
.text()Mono<String>Collected text, enables flatMap chaining
.textStream()Flux<String>Streaming text
.messages()Flux<Message>All message types

Example - Text response with TurnSpec

ClaudeAsyncClient client = ClaudeClient.async()
    .workingDirectory(Path.of("."))
    .permissionMode(PermissionMode.BYPASS_PERMISSIONS)
    .build();

// Simple text response
client.connect("Hello!").text()
    .doOnSuccess(System.out::println)
    .subscribe();  // Non-blocking

Example - Multi-turn with flatMap chaining

// Elegant multi-turn via flatMap
client.connect("My favorite color is blue.").text()
    .flatMap(r1 -> client.query("What is my favorite color?").text())
    .doOnSuccess(System.out::println)  // "blue"
    .subscribe();

Example - Text streaming

// Stream text as it arrives
client.query("Explain recursion").textStream()
    .doOnNext(System.out::print)
    .subscribe();

Example - Full message access

// Access all message types for metadata
client.query("List files").messages()
    .doOnNext(msg -> {
        if (msg instanceof AssistantMessage am) {
            am.text().ifPresent(System.out::println);
        } else if (msg instanceof ResultMessage rm) {
            System.out.printf("Cost: $%.6f%n", rm.totalCostUsd());
        }
    })
    .subscribe();

Example - Spring WebFlux SSE endpoint

import org.springframework.web.bind.annotation.*;
import org.springframework.http.MediaType;
import reactor.core.publisher.Flux;

@RestController
public class ChatController {
    private final ClaudeAsyncClient client;

    public ChatController() {
        this.client = ClaudeClient.async()
            .workingDirectory(Path.of("."))
            .permissionMode(PermissionMode.BYPASS_PERMISSIONS)
            .build();
    }

    @GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chat(@RequestParam String message) {
        return client.query(message).textStream();
    }
}

Example - Cross-turn handlers

ClaudeAsyncClient client = ClaudeClient.async()
    .workingDirectory(Path.of("."))
    .build();

// Register handlers (cross-turn concerns)
client.onMessage(msg -> logger.info("Message: {}", msg.getClass().getSimpleName()))
      .onResult(result -> logger.info("Turn complete, tokens: {}",
          result.usage().inputTokens() + result.usage().outputTokens()));

// Handlers are called for ALL turns
client.queryAndReceive("First question").subscribe();
client.queryAndReceive("Second question").subscribe();  // Handlers still fire

Flux vs Handlers

If you are…Use
Printing or streaming output to a userFlux
Building a WebFlux/SSE endpointFlux
Doing per-turn filtering or transformationFlux
Logging tool usage across all turnsHandlers
Collecting metrics or tracingHandlers
Running a long-lived agent loopHandlers
“If the logic belongs to a single turn, use Flux. If the logic applies to the entire session, use handlers.”

Types

QueryOptions

Configuration for Query operations.
QueryOptions.builder()
    .model("claude-sonnet-4-20250514")      // Model selection
    .systemPrompt("Custom system prompt")    // Replace default
    .appendSystemPrompt("Additional instructions")  // Append to default
    .timeout(Duration.ofMinutes(5))          // Query timeout
    .allowedTools(List.of("Read", "Write"))  // Restrict tools
    .disallowedTools(List.of("Bash"))        // Block tools
    .maxTurns(10)                            // Limit agentic turns
    .maxBudgetUsd(1.0)                       // Cost limit
    .workingDirectory(Path.of("/project"))   // Working directory
    .build();

CLIOptions

Lower-level configuration for CLI transport. Use with ClaudeClient.sync(options) or ClaudeClient.async(options).
CLIOptions options = CLIOptions.builder()
    .model("claude-sonnet-4-20250514")
    .systemPrompt("You are helpful")
    .allowedTools(List.of("Read", "Write"))
    .maxTurns(10)
    .build();

// When using pre-built CLIOptions, only session config is available on builder
try (ClaudeSyncClient client = ClaudeClient.sync(options)
        .workingDirectory(Path.of("."))
        .timeout(Duration.ofMinutes(5))
        .build()) {
    // .model() NOT available here - already in options
}

PermissionMode

public enum PermissionMode {
    DEFAULT,           // Standard permission behavior
    ACCEPT_EDITS,      // Auto-accept file edits
    PLAN,              // Planning mode - read-only
    BYPASS_PERMISSIONS // Bypass all checks (use with caution)
}

HookRegistry

Register callbacks to intercept tool execution.
HookRegistry registry = new HookRegistry();

// Pre-tool hook - runs before tool executes
registry.registerPreToolUse("Bash", input -> {
    // Return HookOutput.allow() or HookOutput.block("reason")
    return HookOutput.allow();
});

// Post-tool hook - runs after tool completes
registry.registerPostToolUse(input -> {
    // Log, trace, or modify behavior
    return HookOutput.allow();
});

McpServerConfig

Configure MCP servers for tool integration.
// External process (npx, docker, etc.)
McpServerConfig external = McpServerConfig.command("npx")
    .args("-y", "@anthropic/mcp-server-filesystem")
    .env("HOME", System.getProperty("user.home"))
    .build();

// In-process SDK server
McpServerConfig inProcess = McpServerConfig.sdk(myMcpServer);

Message Types

ParsedMessage

Sealed interface wrapping messages from the CLI.
public sealed interface ParsedMessage permits
    ParsedMessage.RegularMessage,
    ParsedMessage.Control,
    ParsedMessage.ControlResponseMessage,
    ParsedMessage.EndOfStream {

    boolean isRegularMessage();   // User, Assistant, System, Result
    boolean isControlRequest();   // Control protocol request
    boolean isControlResponse();  // Control protocol response

    Message asMessage();          // Get as Message, or null
    ControlRequest asControlRequest();
}

Processing Messages

Iterator<ParsedMessage> response = client.receiveResponse();
while (response.hasNext()) {
    ParsedMessage parsed = response.next();
    if (parsed.isRegularMessage()) {
        Message message = parsed.asMessage();
        if (message instanceof AssistantMessage am) {
            System.out.println(am.getTextContent().orElse(""));
        } else if (message instanceof ResultMessage result) {
            System.out.println("Done. Cost: $" + result.totalCostUsd());
        }
    }
}

Message

Base interface for regular messages.
public sealed interface Message permits UserMessage, AssistantMessage, ResultMessage

AssistantMessage

public record AssistantMessage(
    List<ContentBlock> content,
    String model
) implements Message {
    public Optional<String> getTextContent();
}

ResultMessage

public record ResultMessage(
    String subtype,
    long durationMs,
    long durationApiMs,
    boolean isError,
    int numTurns,
    String sessionId,
    Double totalCostUsd,
    Usage usage,
    String result
) implements Message

Content Block Types

public sealed interface ContentBlock permits TextBlock, ThinkingBlock, ToolUseBlock, ToolResultBlock

TextBlock

public record TextBlock(String text) implements ContentBlock

ToolUseBlock

public record ToolUseBlock(
    String id,
    String name,
    Map<String, Object> input
) implements ContentBlock

Error Types

public class ClaudeSDKException extends RuntimeException {
    // Base exception for all SDK errors
}

public class TransportException extends ClaudeSDKException {
    // Raised when CLI transport fails
}

public class SessionClosedException extends ClaudeSDKException {
    // Raised when operating on a closed session
}

public class MessageParseException extends ClaudeSDKException {
    // Raised when message parsing fails
}

See also