Skip to content

ethpandaops/claude-agent-sdk-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Claude Agent SDK Go

Go SDK for building agentic applications with Claude Code.

Installation

go get github.com/ethpandaops/claude-agent-sdk-go

Prerequisites:

  • Go 1.26+
  • Claude Code CLI v2.1.59+ (npm install -g @anthropic-ai/claude-code)

Tested against Claude Code CLI 2.1.89.

Quick Start

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)
            }
        }
    }
}

Basic Usage: Query()

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
}

Model Discovery

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"])
}

With Options

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
}

With Tools

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
}

Permission Modes

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.

Permission Callback Context

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.

Persisted Permissions

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
    }),
}

Tool Metadata Helpers

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.

QueryStream Heartbeats

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.

Client (Multi-turn)

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).

SDK MCP Servers (Custom Tools)

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
}

Multimodal Input

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 prompts
  • image blocks via ImageInput(...) / ImageFileInput(...)
  • document PDF blocks via PDFInput(...) / PDFFileInput(...)
  • @/absolute/path mentions via PathInput(...)

See examples/multimodal_input for a complete example that runs both path-based attachments and inline image/PDF blocks.

Hooks

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
}

Streaming Input Priority

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
}

Fast Mode

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()
_ = tracker

MCP Elicitation

Use 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
}

Plan Mode User Input

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
}

Prompt Dialogs

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
}

Cost Tracking

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})
_ = budget
tracker := 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.

Context Pressure Helpers

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)
}

Rewind Files

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.

Runtime Environment Overrides

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(...).

Error Classification Helpers

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.

Recovery Planning

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.

Turn Completion Callback

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.

Structured Output Helpers

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)
    }
}

Result Messages

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)
    }
}

Resume Metadata Helpers

NewPreservedSegmentIndex() records CompactBoundaryMessage preserved segments so hosts can resolve relinked UUIDs across compaction boundaries during resume/fork flows.

Discovery Helpers

DiscoverSessionMetadata(serverInfo, initMsg) combines slash commands, skills, plugins, output style, and fast-mode state into one helper view for host UIs.

Transport Health

Interactive clients now expose GetTransportHealth() so hosts can inspect parse/send/read failure state without waiting for a terminal client error.

Types

Core message types implement the Message interface:

  • UserMessage - User input
  • AssistantMessage - Claude response with Content []ContentBlock
  • ResultMessage - Final result with Result string
  • SystemMessage - System messages

Content blocks: TextBlock, InputImageBlock, InputDocumentBlock, ThinkingBlock, ToolUseBlock, ToolResultBlock

See types.go for complete type definitions.

Static Model Catalog

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.

Error Handling

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 found
  • CLIConnectionError - Failed to connect to CLI
  • ProcessError - CLI process failure
  • MessageParseError - Message parsing failure
  • CLIJSONDecodeError - JSON decode failure

Sentinel errors: ErrClientNotConnected, ErrClientAlreadyConnected, ErrClientClosed, ErrSessionNotFound

Session Metadata

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)

Examples

See the examples directory for complete working examples.

Testing 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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors