Go SDK for building agentic applications with Claude Code.
go get github.com/ethpandaops/claude-agent-sdk-goPrerequisites:
- Go 1.26+
- Claude Code CLI v2.1.59+ (
npm install -g @anthropic-ai/claude-code)
Tested against Claude Code CLI 2.1.89.
package main
import (
"context"
"fmt"
claudesdk "github.com/ethpandaops/claude-agent-sdk-go"
)
func main() {
ctx := context.Background()
for msg, err := range claudesdk.Query(ctx, claudesdk.Text("What is 2 + 2?")) {
if err != nil {
panic(err)
}
if result, ok := msg.(*claudesdk.ResultMessage); ok {
if result.Result != nil {
fmt.Println(*result.Result)
}
}
}
}One-shot query execution returning an iterator of messages.
Query() accepts UserMessageContent.
Use Text(...) for plain prompts and Blocks(...) for structured multimodal input.
Text-only queries use Claude CLI --print mode. Structured content uses streaming mode.
for msg, err := range claudesdk.Query(ctx, claudesdk.Text("Explain Go interfaces")) {
// handle msg
}ListModels(ctx) returns the SDK's static Claude model catalog in a Codex-like
payload shape. It is not a live per-user model list from the Claude CLI.
models, err := claudesdk.ListModels(ctx)
if err != nil {
panic(err)
}
for _, model := range models {
fmt.Println(model.ID, model.Metadata["modelContextWindow"])
}for msg, err := range claudesdk.Query(ctx, claudesdk.Text("Hello"),
claudesdk.WithSystemPrompt("You are a helpful assistant"),
claudesdk.WithModel("claude-sonnet-4-6"),
claudesdk.WithMaxTurns(3),
) {
// handle msg
}for msg, err := range claudesdk.Query(ctx, claudesdk.Text("Create hello.py that prints hello world"),
claudesdk.WithAllowedTools("Read", "Write"),
claudesdk.WithPermissionMode(string(claudesdk.PermissionModeAcceptEdits)),
) {
// handle msg
}Supported permission modes are default, acceptEdits, plan, auto, dontAsk, and bypassPermissions.
opts := []claudesdk.Option{
claudesdk.WithPermissionMode(string(claudesdk.PermissionModeDontAsk)),
claudesdk.WithMaxTurns(1),
}Use dontAsk for headless or CI workflows where permission prompts must be denied instead of surfaced interactively.
WithCanUseTool(...) now receives richer typed context from Claude Code, including permission suggestions, blocked_path, raw decision_reason, structured DecisionReasonType when it can be derived, UI labels like title, display_name, and description, plus derived ToolCategory, ConcurrencySafe, and InterruptBehavior hints for the requested tool. Sandbox network permission prompts are surfaced as the synthetic tool name claudesdk.SandboxNetworkAccessToolName.
WithPersistPermissions(path) stores SDK-managed permission rules on disk and reapplies them across future sessions before your WithCanUseTool(...) callback runs. The SDK preserves update destinations such as userSettings, projectSettings, localSettings, and cliArg inside its own file and evaluates them with destination precedence, without depending on Claude CLI settings-file internals.
opts := []claudesdk.Option{
claudesdk.WithPersistPermissions(".claude-sdk-permissions.json"),
claudesdk.WithCanUseTool(func(
_ context.Context,
toolName string,
_ map[string]any,
_ *claudesdk.ToolPermissionContext,
) (claudesdk.PermissionResult, error) {
if toolName == "Bash" {
behavior := claudesdk.PermissionBehaviorAllow
destination := claudesdk.PermissionUpdateDestUserSettings
return &claudesdk.PermissionResultAllow{
Behavior: "allow",
UpdatedPermissions: []*claudesdk.PermissionUpdate{{
Type: claudesdk.PermissionUpdateTypeAddRules,
Rules: []*claudesdk.PermissionRuleValue{{
ToolName: "Bash",
}},
Behavior: &behavior,
Destination: &destination,
}},
}, nil
}
return &claudesdk.PermissionResultAllow{Behavior: "allow"}, nil
}),
}ClassifyToolCategory(toolName), IsConcurrencySafeTool(toolName), and InterruptBehaviorForTool(toolName) expose the SDK's best-effort tool metadata for hosts building their own orchestration or streamlined UIs. Parsed ToolUseBlocks and ToolPermissionContexts also carry the same derived category/concurrency/interrupt metadata directly.
ConnectorTextBlock preserves IDE/connector-synchronized text blocks as a first-class content block instead of degrading to UnknownBlock.
TaskLifecycleTracker aggregates task_started, task_progress, and task_notification messages into a per-task snapshot view, including ActiveTasks(), TerminalTasks(), and Summary(). ModelHasOneMillionContextSuffix(...) / EffectiveModelContextWindow(...) add a small helper layer for explicit [1m] model IDs, and parsed InitMessage values now carry a derived ContextWindow when the SDK can resolve it from the live init payload, beta flags, or model ID.
Interactive Client sessions already send periodic keep_alive heartbeats. QueryStream(...) now does the same while it is still streaming input or waiting for a terminal result in callback-driven sessions, which makes long-running headless stream-json flows less fragile.
WithClient manages the client lifecycle for multi-turn conversations. Client.GetServerInfo() now returns typed initialize metadata.
err := claudesdk.WithClient(ctx, func(c claudesdk.Client) error {
if err := c.Query(ctx, claudesdk.Text("Hello, remember my name is Alice")); err != nil {
return err
}
for msg, err := range c.ReceiveResponse(ctx) {
if err != nil {
return err
}
// handle response
}
if err := c.Query(ctx, claudesdk.Text("What's my name?")); err != nil {
return err
}
for msg, err := range c.ReceiveResponse(ctx) {
if err != nil {
return err
}
// Claude remembers: "Alice"
_ = msg
}
info, err := c.GetServerInfo()
if err != nil {
return err
}
fmt.Println(info.OutputStyle)
return nil
})Client.Interrupt(ctx) is now a soft interrupt by default. If your host wants to synthesize error tool_result blocks for unresolved tool uses before interrupting, opt in with WithInterruptToolResultPolicy(claudesdk.InterruptToolResultPolicySynthesizeErrorToolResults).
Create in-process tools using the Model Context Protocol.
schema := claudesdk.SimpleSchema(map[string]string{"a": "number", "b": "number"})
handler := func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args, _ := claudesdk.ParseArguments(req)
sum := args["a"].(float64) + args["b"].(float64)
return claudesdk.TextResult(fmt.Sprintf("%.0f", sum)), nil
}
tool := claudesdk.NewSdkMcpTool("add", "Add two numbers", schema, handler)
server := claudesdk.CreateSdkMcpServer("calc", "1.0.0", tool)
for msg, err := range claudesdk.Query(ctx, claudesdk.Text("Calculate 2 + 2"),
claudesdk.WithMCPServers(map[string]claudesdk.MCPServerConfig{"calc": server}),
claudesdk.WithAllowedTools("mcp__calc__add"),
) {
// handle msg
}The SDK now supports structured Claude CLI user content:
content := claudesdk.Blocks(
claudesdk.TextInput("Describe these attachments briefly."),
claudesdk.PathInput("/absolute/path/to/local-image.png"),
)
for msg, err := range claudesdk.Query(ctx, content) {
// handle msg
}Inline helpers are also available for fully programmatic payloads:
img, _ := claudesdk.ImageFileInput("/absolute/path/to/local-image.png")
pdf, _ := claudesdk.PDFFileInput("/absolute/path/to/spec.pdf")
content := claudesdk.Blocks(
claudesdk.TextInput("Summarize the PDF and image."),
img,
pdf,
)
for msg, err := range claudesdk.Query(ctx, content) {
// handle msg
}Supported CLI formats validated by this SDK:
Text(...)for plain promptsimageblocks viaImageInput(...)/ImageFileInput(...)documentPDF blocks viaPDFInput(...)/PDFFileInput(...)@/absolute/pathmentions viaPathInput(...)
See examples/multimodal_input for a complete example that runs both path-based attachments and inline image/PDF blocks.
Intercept and modify tool execution.
handler := func(ctx context.Context, hookCtx claudesdk.HookContext, input claudesdk.HookInput) (claudesdk.HookJSONOutput, error) {
pre := input.(*claudesdk.PreToolUseHookInput)
fmt.Printf("Tool: %s\n", pre.ToolName)
return &claudesdk.SyncHookJSONOutput{}, nil
}
for msg, err := range claudesdk.Query(ctx, claudesdk.Text(prompt),
claudesdk.WithHooks(map[claudesdk.HookEvent][]*claudesdk.HookMatcher{
claudesdk.HookEventPreToolUse: {{
ToolName: "Bash",
Hooks: []claudesdk.HookCallback{handler},
}},
}),
) {
// handle msg
}QueryStream and StartWithStream accept StreamingMessage.Priority so you can enqueue now, next, or later messages explicitly.
messages := claudesdk.MessagesFromSlice([]claudesdk.StreamingMessage{
claudesdk.NewUserMessageWithPriority(
claudesdk.Text("Reply with one word."),
claudesdk.StreamingMessagePriorityNow,
),
})
for msg, err := range claudesdk.QueryStream(ctx, messages,
claudesdk.WithPermissionMode(string(claudesdk.PermissionModeDontAsk)),
) {
_, _ = msg, err
}WithFastMode(true) opts headless SDK sessions into fast mode through the CLI settings layer. WithFastModeTracker(...) adds a lightweight host-side tracker that observes init/result/rate-limit/retry messages and derives off / on / cooldown transitions. WithUnattendedRetry(true) sets CLAUDE_CODE_UNATTENDED_RETRY=1 for long-running autonomous sessions while still leaving retry execution to the CLI.
client := claudesdk.NewClient()
defer client.Close()
err := client.Start(ctx,
claudesdk.WithModel("claude-opus-4-6"),
claudesdk.WithFastMode(true),
)
if err != nil {
log.Fatal(err)
}
info, _ := client.GetServerInfo()
if info.FastModeState != nil {
fmt.Println(*info.FastModeState)
}
tracker := claudesdk.NewFastModeTracker()
_ = trackerUse WithOnElicitation to accept, decline, or cancel structured MCP prompts during a session. Pair it with WithOnElicitationComplete when you also need completion notifications for URL-mode or deferred flows.
onElicitation := func(_ context.Context, req *claudesdk.ElicitationRequest) (*claudesdk.ElicitationResponse, error) {
return &claudesdk.ElicitationResponse{
Action: claudesdk.ElicitationActionAccept,
Content: map[string]any{"approved": true},
}, nil
}
onComplete := func(_ context.Context, completion *claudesdk.ElicitationCompleteMessage) error {
fmt.Println(completion.ElicitationID)
return nil
}Handle AskUserQuestion prompts with a typed callback:
onUserInput := func(
_ context.Context,
req *claudesdk.UserInputRequest,
) (*claudesdk.UserInputResponse, error) {
answers := map[string]*claudesdk.UserInputAnswer{}
for _, q := range req.Questions {
if len(q.Options) > 0 {
answers[q.Question] = &claudesdk.UserInputAnswer{
Answers: []string{q.Options[0].Label},
}
}
}
return &claudesdk.UserInputResponse{Answers: answers}, nil
}
for msg, err := range claudesdk.Query(ctx,
claudesdk.Text("Use AskUserQuestion to ask me to pick Go or Rust, then confirm my choice."),
claudesdk.WithPermissionMode("plan"),
claudesdk.WithOnUserInput(onUserInput),
) {
_ = msg
_ = err
}WithOnPrompt handles the separate prompt request/response protocol used for generic multiple-choice dialogs. This is distinct from WithOnUserInput, which is specific to the AskUserQuestion tool.
onPrompt := func(_ context.Context, req *claudesdk.PromptRequest) (*claudesdk.PromptResponse, error) {
return &claudesdk.PromptResponse{Selected: req.Options[0].Key}, nil
}CostTracker now aggregates terminal-result durations, turns, token/cache/web-search totals, and can persist session-cost state under Claude home when attached with WithCostTracker(...). BudgetTracker builds on the same result stream to flag near-budget turns, diminishing returns, and continuation pressure without interrupting the session for you. EvaluateBudgetProgress(...) exposes the same host-side recommendation logic as a pure helper. CostTracker, BudgetTracker, FastModeTracker, CompactionTracker, and TaskLifecycleTracker are safe for concurrent observation from multiple goroutines.
maxCost := 2.0
budget, _ := claudesdk.NewBudgetTracker(claudesdk.BudgetTrackerOptions{MaxCostUSD: &maxCost})
_ = budgettracker := claudesdk.NewCostTracker()
for msg, err := range claudesdk.Query(ctx, claudesdk.Text("Say hi"),
claudesdk.WithCostTracker(tracker),
) {
if err != nil {
log.Fatal(err)
}
tracker.Observe(msg) // safe alongside WithCostTracker; results are deduplicated by UUID
}
snapshot := tracker.Snapshot()
fmt.Println(snapshot.Results, snapshot.TotalDurationMs, snapshot.TotalInputTokens)Persisted session-cost snapshots can also be loaded or deleted directly with LoadSessionCost, SaveSessionCost, and DeleteSessionCost.
EvaluateContextPressure adds source-aligned warning, error, auto-compact, and blocking tiers on top of GetContextUsage(). NewAutoCompactFailureCircuitBreaker(...) tracks repeated compaction failures so hosts can stop retrying after a configured streak, and NewAutoCompactOrchestrator(...) combines tier evaluation with the breaker into a host-facing compaction recommendation.
CompactionTracker observes StatusMessage, CompactBoundaryMessage, and terminal ResultMessage values so hosts can track compaction boundaries and infer when the next turn likely rebuilt prompt cache (LastCacheBreakLikely based on cache-creation token usage).
usage, _ := client.GetContextUsage(ctx)
status, _ := claudesdk.EvaluateContextPressure(usage, claudesdk.DefaultContextPressurePolicy())
if status.ShouldAutoCompact {
fmt.Println("compact soon", status.RemainingTokens)
}Client.RewindFiles(...) returns a typed RewindFilesResult with CanRewind, FilesChanged, Insertions, and Deletions.
result, err := client.RewindFiles(ctx, userMessageID)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.CanRewind, result.FilesChanged)WithEnableFileCheckpointing(true) automatically sets CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true for SDK-launched CLI processes.
The SDK now exposes typed options for several Claude Code environment overrides:
WithMaxToolUseConcurrency(...)WithAutoCompactPercentageOverride(...)WithBlockingLimitOverride(...)WithDisableCompact(...)WithDisableAutoCompact(...)
These map to the corresponding CLI environment variables for hosts that need to tune runtime behavior without using raw WithEnv(...).
ClassifyResultError(result) and ClassifyAPIRetry(msg) provide typed SDK-side classification for prompt-too-long, media-too-large, rate-limit, overload, auth, billing, and generic execution failures. Each classification now includes a RecoveryAction, optional ErrorSubClass, and best-effort RetryAfterSeconds.
DefaultRetryPolicy(), EvaluateRetry(msg, policy), and EvaluateResultRetry(result, attempt, policy) add host-side retry guidance without auto-replaying turns for you. These are optional host-policy helpers rather than protocol guarantees.
RecoveryOrchestrator turns terminal ResultMessage classifications into a host-side recovery plan. PlanResult(...) can recommend media stripping, fallback-model switching, or retry-with-delay using the SDK retry policy, and ResilientQuery(...) wraps Query(...) with that planning loop for one-shot host workflows.
WithOnTurnComplete(...) fires after each terminal ResultMessage with lightweight turn cost, aggregate cost, and side-channel summary data such as prompt suggestions, post-turn summaries, and task notifications seen during that turn.
DecodeStructuredOutput[T](result) decodes ResultMessage.StructuredOutput into a typed Go value and returns ErrStructuredOutputMissing when the result did not include parsed structured output.
if result, ok := msg.(*claudesdk.ResultMessage); ok {
person, err := claudesdk.DecodeStructuredOutput[Person](result)
if err == nil {
fmt.Println(person.Name)
}
}ResultMessage.Subtype is now a typed ResultSubtype. Compare it against exported constants such as ResultSubtypeSuccess, ResultSubtypeErrorMaxTurns, and ResultSubtypeErrorMaxBudgetUSD. ResultMessage.ModelUsage is also typed as map[string]ModelUsage for per-model token and cost accounting.
if result, ok := msg.(*claudesdk.ResultMessage); ok {
if result.Subtype == claudesdk.ResultSubtypeErrorMaxBudgetUSD {
fmt.Println("budget exceeded")
}
if usage, ok := result.ModelUsage["claude-sonnet-4-6"]; ok {
fmt.Println(usage.InputTokens, usage.OutputTokens)
}
}NewPreservedSegmentIndex() records CompactBoundaryMessage preserved segments so hosts can resolve relinked UUIDs across compaction boundaries during resume/fork flows.
DiscoverSessionMetadata(serverInfo, initMsg) combines slash commands, skills, plugins, output style, and fast-mode state into one helper view for host UIs.
Interactive clients now expose GetTransportHealth() so hosts can inspect parse/send/read failure state without waiting for a terminal client error.
Core message types implement the Message interface:
UserMessage- User inputAssistantMessage- Claude response withContent []ContentBlockResultMessage- Final result withResult stringSystemMessage- System messages
Content blocks: TextBlock, InputImageBlock, InputDocumentBlock, ThinkingBlock, ToolUseBlock, ToolResultBlock
See types.go for complete type definitions.
Models(), ModelByID(), and ListModels(ctx) use a static SDK catalog, not a
live Claude CLI model list for the logged-in user.
The catalog includes the current concrete model IDs exposed by this SDK.
SDK errors can be inspected using errors.AsType (Go 1.26+):
if cliErr, ok := errors.AsType[*claudesdk.CLINotFoundError](err); ok {
fmt.Println("Claude CLI not found:", cliErr)
}
if procErr, ok := errors.AsType[*claudesdk.ProcessError](err); ok {
fmt.Println("Process failed:", procErr)
}Error types:
CLINotFoundError- Claude CLI binary not foundCLIConnectionError- Failed to connect to CLIProcessError- CLI process failureMessageParseError- Message parsing failureCLIJSONDecodeError- JSON decode failure
Sentinel errors: ErrClientNotConnected, ErrClientAlreadyConnected, ErrClientClosed, ErrSessionNotFound
Inspect locally persisted session metadata without sending a prompt:
stat, err := claudesdk.StatSession(ctx, sessionID,
claudesdk.WithStatProjectPath("/path/to/project"), // optional
claudesdk.WithStatClaudeHome("/custom/.claude"), // optional
)
if err != nil {
if errors.Is(err, claudesdk.ErrSessionNotFound) {
// session does not exist in local persistence
}
// handle other errors
}
fmt.Println(stat.SizeBytes, stat.LastModified)See the examples directory for complete working examples.
The scripts/test_examples.sh script runs all examples and uses Claude CLI to verify their output.
# Run all examples
./scripts/test_examples.sh
# Run specific examples
./scripts/test_examples.sh -f hooks,sessions,tools_option
# Keep going on failure
./scripts/test_examples.sh -k
# Adjust parallelism and timeout
./scripts/test_examples.sh -n 3 -t 180