Skip to contents

Unofficial community port. Not affiliated with or endorsed by Anthropic. This is an R implementation of Anthropic’s claude-code-sdk.

R SDK for Claude Agent. Mirrors the Python SDK with idiomatic R internals. See the Claude Agent SDK documentation for more information.

Installation

# Install from GitHub
remotes::install_github("kaipingyang/ClaudeAgentSDK")

# Or from source (development)
devtools::install("/path/to/ClaudeAgentSDK")

Prerequisites:

  • R 4.1+
  • Required packages: processx, jsonlite, R6, later, coro, rlang, cli

Note: The Claude Code CLI must be installed separately:

curl -fsSL https://claude.ai/install.sh | bash
# or
npm install -g @anthropic-ai/claude-code

You can specify a custom path via ClaudeAgentOptions(cli_path = "/path/to/claude").

Quick Start

library(ClaudeAgentSDK)


# Simple one-shot query (synchronous)
result <- claude_run("What is 2 + 2?")
print(result)

# Stream messages with claude_query()
gen <- claude_query("What is 2 + 2?")
coro::loop(for (msg in gen) {
  print(msg)
})

Basic Usage: claude_query() and claude_run()

claude_query() is the R equivalent of Python’s query(). It returns a coro generator that yields typed message objects. See R/query.R.

library(ClaudeAgentSDK)

# Stream through messages
gen <- claude_query("Hello Claude")
coro::loop(for (msg in gen) {
  if (inherits(msg, "AssistantMessage")) {
    for (block in msg$content) {
      if (inherits(block, "TextBlock")) {
        cat(block$text)
      }
    }
  }
})

# With options
options <- ClaudeAgentOptions(
  system_prompt = "You are a helpful assistant",
  max_turns     = 1L
)

gen <- claude_query("Tell me a joke", options = options)
coro::loop(for (msg in gen) {
  print(msg)
})

Synchronous helper: claude_run()

For simple blocking use, claude_run() collects all messages and returns a ClaudeRunResult:

result <- claude_run("What is 2 + 2?",
  options = ClaudeAgentOptions(max_turns = 1L))

# Access the ResultMessage
print(result$result)            # <ResultMessage ...>
cat(result$result$total_cost_usd, "\n")

# Walk all messages
for (msg in result$messages) {
  if (inherits(msg, "AssistantMessage")) {
    cat(msg$content[[1]]$text, "\n")
  }
}

Using Tools

By default, Claude has access to the full Claude Code toolset (Read, Write, Edit, Bash, and others). allowed_tools is a permission allowlist: listed tools are auto-approved, and unlisted tools fall through to permission_mode and can_use_tool for a decision. To block specific tools use disallowed_tools.

options <- ClaudeAgentOptions(
  allowed_tools   = c("Read", "Write", "Bash"),   # auto-approve these
  permission_mode = "acceptEdits"                 # auto-accept file edits
)

gen <- claude_query("Create a hello.R file", options = options)
coro::loop(for (msg in gen) {
  # process tool use and results
})

Working Directory

options <- ClaudeAgentOptions(
  cwd = "/path/to/project"
)

ClaudeSDKClient

ClaudeSDKClient is an R6 class that supports bidirectional, interactive conversations with Claude Code. See R/client.R.

Unlike claude_query(), ClaudeSDKClient additionally enables hooks and runtime control (interrupt, permission-mode changes, model switching).

Interactive Client

library(ClaudeAgentSDK)

client <- ClaudeSDKClient$new(
  ClaudeAgentOptions(cwd = getwd())
)
client$connect()

# Send a prompt and receive the response (query() is an alias for send())
client$query("What files are in the current directory?")
coro::loop(for (msg in client$receive_response()) {
  if (inherits(msg, "AssistantMessage")) {
    for (block in msg$content) {
      if (inherits(block, "TextBlock")) cat(block$text)
    }
  }
  if (inherits(msg, "ResultMessage")) {
    cat("\nCost: $", msg$total_cost_usd, "\n")
  }
})

# Send a follow-up in the same session
client$query("Now count how many R files there are.")
coro::loop(for (msg in client$receive_response()) {
  if (inherits(msg, "AssistantMessage")) {
    cat(msg$content[[1]]$text, "\n")
  }
})

client$disconnect()

Hooks

A hook is an R function that the Claude Code application invokes at specific points of the agent loop. Hooks can intercept tool calls, validate commands, and provide deterministic feedback.

library(ClaudeAgentSDK)

check_bash_command <- function(input_data, tool_use_id, context) {
  if (!identical(input_data$hook_event_name, "PreToolUse")) return(list())
  command <- input_data$tool_input$command %||% ""
  block_patterns <- c("rm -rf", "sudo")
  for (pattern in block_patterns) {
    if (grepl(pattern, command, fixed = TRUE)) {
      return(list(
        hookSpecificOutput = list(
          hookEventName         = "PreToolUse",
          permissionDecision    = "deny",
          permissionDecisionReason = paste("Command contains blocked pattern:", pattern)
        )
      ))
    }
  }
  list()
}

options <- ClaudeAgentOptions(
  allowed_tools = "Bash",
  hooks = list(
    PreToolUse = list(
      list(
        matcher = "Bash",
        hooks   = list(check_bash_command)
      )
    )
  )
)

client <- ClaudeSDKClient$new(options)
client$connect()

# Test 1: Command with forbidden pattern (will be blocked)
client$send("Run: rm -rf /tmp/test")
coro::loop(for (msg in client$receive_response()) {
  print(msg)
})

# Test 2: Safe command
client$send("Run: echo 'Hello from hooks!'")
coro::loop(for (msg in client$receive_response()) {
  if (inherits(msg, "AssistantMessage")) cat(msg$content[[1]]$text, "\n")
})

client$disconnect()

Permission Callbacks

Use can_use_tool for programmatic tool permission control:

my_permission <- function(tool_name, tool_input, context) {
  if (tool_name == "Bash") {
    cmd <- tool_input$command %||% ""
    if (grepl("sudo", cmd, fixed = TRUE)) {
      return(PermissionResultDeny("sudo commands are not allowed"))
    }
  }
  PermissionResultAllow()
}

options <- ClaudeAgentOptions(
  can_use_tool = my_permission
)

Tool Approval (Shiny)

Set permission_prompt_tool_name = "stdio" to receive PermissionRequestMessage objects in your message loop. Call approve_tool() / deny_tool() from your UI event handlers (e.g., button clicks):

library(coro); library(promises)

client <- ClaudeSDKClient$new(ClaudeAgentOptions(
  permission_prompt_tool_name = "stdio",
  include_partial_messages    = TRUE
))
client$connect()

pending_id <- reactiveVal(NULL)

do_stream <- coro::async(function(client, pending_id, session) {
  repeat {
    msgs <- tryCatch(client$poll_messages(), error = function(e) list())
    if (length(msgs) == 0L) {
      await(promises::promise(function(resolve, reject) {
        later::later(function() resolve(TRUE), 0.05)
      }))
      next
    }
    for (msg in msgs) {
      await(promises::promise_resolve(TRUE))
      if (inherits(msg, "PermissionRequestMessage")) {
        pending_id(msg$request_id)
        showModal(modalDialog(
          title  = paste("Allow tool:", msg$tool_name),
          footer = tagList(
            actionButton("tool_allow", "Allow", class = "btn-success"),
            actionButton("tool_deny",  "Deny",  class = "btn-danger")
          )
        ), session = session)
        next
      }
      if (inherits(msg, "ResultMessage")) return("done")
    }
  }
})

# In Shiny server:
observeEvent(input$tool_allow, {
  rid <- pending_id()
  if (!is.null(rid)) { pending_id(NULL); removeModal(); client$approve_tool(rid) }
})
observeEvent(input$tool_deny, {
  rid <- pending_id()
  if (!is.null(rid)) { pending_id(NULL); removeModal(); client$deny_tool(rid, "Denied") }
})

See the Shiny Tool Approval article for a full walkthrough, or browse examples/15_shinychat_tool_approval_msgdriven.R directly. Additional experimental UI patterns (inline, conversational, insertUI, tool cards) are available in examples 16–19.

For the most complete pattern — streaming thinking cards + tool approval + interrupt — see the Shiny Streaming Thinking article or examples/20_shinychat_streaming_thinking.R.

Runtime Control

client <- ClaudeSDKClient$new(ClaudeAgentOptions())
client$connect()

# Change permission mode at runtime
client$set_permission_mode("acceptEdits")

# Interrupt a running operation
client$interrupt()

# Switch model mid-session
client$set_model("claude-sonnet-4-6")

# Get MCP server status
status <- client$get_mcp_status()
cat("MCP servers:", length(status$mcpServers), "\n")

# Get context usage
usage <- client$get_context_usage()
cat("Context:", usage$percentage, "% used\n")

client$disconnect()

Types

See R/types.R for complete type definitions:

  • ClaudeAgentOptions — Configuration options
  • AssistantMessage, UserMessage, SystemMessage, ResultMessage — Message types
  • TextBlock, ToolUseBlock, ToolResultBlock, ThinkingBlock — Content blocks
  • StreamEvent, RateLimitEvent — Streaming events
  • TaskStartedMessage, TaskProgressMessage, TaskNotificationMessage — Task messages
  • PermissionResultAllow, PermissionResultDeny, PermissionRequestMessage, PermissionUpdate, PermissionRuleValue — Permission types
  • PreToolUseHookInput, PostToolUseHookInput, … — Hook input types (10 total)
  • SyncHookOutput, AsyncHookOutput — Hook output types
  • SystemPromptPreset, SystemPromptFile — System prompt types
  • SandboxSettings, SandboxNetworkConfig, SandboxIgnoreViolations — Sandbox types
  • ThinkingConfigAdaptive, ThinkingConfigEnabled, ThinkingConfigDisabled — Thinking config
  • TaskBudget, TaskUsage, ContextUsageCategory, ContextUsageResponse — Budget/usage types
  • AgentDefinition, HookMatcher — Agent and hook configuration
  • SDKSessionInfo, SessionMessage — Session types

All objects are S3 lists with a class attribute; use inherits(msg, "AssistantMessage") for type checks.

Error Handling

library(ClaudeAgentSDK)

tryCatch(
  {
    result <- claude_run("Hello")
  },
  claude_error_cli_not_found = function(e) {
    cat("Please install Claude Code\n")
  },
  claude_error_process = function(e) {
    cat("Process failed with exit code:", e$exit_code, "\n")
  },
  claude_error_json_decode = function(e) {
    cat("Failed to parse response:", e$line, "\n")
  },
  claude_error = function(e) {
    cat("SDK error:", conditionMessage(e), "\n")
  }
)

Error classes

R class Equivalent Python When raised
claude_error ClaudeSDKError Base class for all SDK errors
claude_error_cli_not_found CLINotFoundError Claude Code CLI not found
claude_error_cli_connection CLIConnectionError Connection/startup failure
claude_error_process ProcessError CLI process exited with error
claude_error_json_decode CLIJSONDecodeError Malformed JSON from CLI
claude_error_message_parse MessageParseError Unrecognized message structure

Session Management

library(ClaudeAgentSDK)

# List recent sessions for a project
sessions <- list_sessions(
  directory = "/path/to/project",
  limit     = 10L
)
for (s in sessions) {
  cat(s$session_id, "-", s$summary, "\n")
}

# List all sessions across all projects
all_sessions <- list_sessions()

# Get metadata for a specific session
info <- get_session_info("550e8400-e29b-41d4-a716-446655440000")
if (!is.null(info)) cat(info$summary, "\n")

# Get conversation messages from a session
messages <- get_session_messages(
  "550e8400-e29b-41d4-a716-446655440000",
  directory = "/path/to/project"
)
for (m in messages) {
  cat(m$type, ":", m$uuid, "\n")
}

Advanced: Continuing and Resuming Sessions

# Continue the most recent session
result <- claude_run(
  "Continue where we left off",
  options = ClaudeAgentOptions(continue_conversation = TRUE)
)

# Resume a specific session
result <- claude_run(
  "What did we discuss?",
  options = ClaudeAgentOptions(resume = "550e8400-e29b-41d4-a716-446655440000")
)

# Fork a session (resume into a new session ID)
result <- claude_run(
  "Try a different approach",
  options = ClaudeAgentOptions(
    resume       = "550e8400-e29b-41d4-a716-446655440000",
    fork_session = TRUE
  )
)

Advanced: Thinking and Effort

# Extended thinking (adaptive) — using typed constructor or plain list
options <- ClaudeAgentOptions(
  thinking = ThinkingConfigAdaptive()
)

# Extended thinking with budget
options <- ClaudeAgentOptions(
  thinking = ThinkingConfigEnabled(budget_tokens = 10000L)
)

# Disable thinking
options <- ClaudeAgentOptions(
  thinking = ThinkingConfigDisabled()
)

# Effort level
options <- ClaudeAgentOptions(effort = "high")

Advanced: Structured Output

options <- ClaudeAgentOptions(
  output_format = list(
    type   = "json_schema",
    schema = list(
      type       = "object",
      properties = list(
        answer = list(type = "string"),
        steps  = list(type = "array", items = list(type = "string"))
      )
    )
  )
)

result <- claude_run("Explain 2+2 step by step", options = options)

Advanced: MCP Servers

options <- ClaudeAgentOptions(
  mcp_servers = list(
    my_server = list(
      type    = "stdio",
      command = "python",
      args    = c("-m", "my_mcp_server")
    )
  ),
  allowed_tools = "mcp__my_server__my_tool"
)

Advanced: Custom Tools via MCP

The Python SDK provides create_sdk_mcp_server() for defining tools in-process. The R SDK uses mcptools instead, which runs tools in a separate R subprocess via the standard MCP protocol. Functionally equivalent — the only difference is shared-memory access.

# 1. Define tools in a file (e.g., mcp_tools_def.R)
list(
  ellmer::tool(
    fun         = function(a, b) a + b,
    name        = "add",
    description = "Add two numbers",
    arguments   = list(
      a = ellmer::type_number("First number"),
      b = ellmer::type_number("Second number")
    )
  )
)

# 2. Launch as an MCP server and pass to ClaudeAgentOptions
options <- ClaudeAgentOptions(
  mcp_servers = r_mcp_server("mcp_tools_def.R"),
  allowed_tools = "mcp__r_tools__add"
)

result <- claude_run("What is 3 + 4?", options = options)

See examples/11_mcp_server.R for a complete example.

Advanced: Async / Shiny Integration

The recommended Shiny pattern uses coro::async with poll_messages() inside an ExtendedTask. Each await() call yields the R event loop, allowing Shiny input events (interrupt button, approval buttons) to fire between tokens.

library(coro); library(promises); library(shinychat)

client <- ClaudeSDKClient$new(ClaudeAgentOptions(
  max_turns                = 5L,
  include_partial_messages = TRUE
))
client$connect()
onStop(function() client$disconnect())

interrupt_flag <- reactiveVal(FALSE)

do_stream <- coro::async(function(client, interrupt_flag, session) {
  chunk_started <- FALSE
  interrupted   <- FALSE

  repeat {
    if (!interrupted && shiny::isolate(interrupt_flag())) {
      interrupted <- TRUE
      tryCatch(client$interrupt(), error = function(e) NULL)
    }

    msgs <- tryCatch(client$poll_messages(), error = function(e) list())
    if (length(msgs) == 0L) {
      await(promises::promise(function(resolve, reject) {
        later::later(function() resolve(TRUE), 0.05)
      }))
      next
    }

    drain_done <- FALSE
    for (msg in msgs) {
      await(promises::promise_resolve(TRUE))  # yield between each message

      # drain after interrupt: skip until ResultMessage
      if (interrupted) {
        if (inherits(msg, "ResultMessage")) { drain_done <- TRUE; break }
        next
      }

      # stream text tokens
      if (inherits(msg, "StreamEvent") && is.list(msg$event)) {
        evt <- msg$event
        if (identical(evt$type, "content_block_delta") &&
            identical(evt$delta$type, "text_delta")) {
          if (!chunk_started) {
            chunk_started <- TRUE
            chat_append_message("chat",
              list(role = "assistant", content = ""),
              chunk = "start", session = session)
          }
          chat_append_message("chat",
            list(role = "assistant", content = evt$delta$text),
            chunk = TRUE, session = session)
        }
      }

      if (inherits(msg, "ResultMessage")) {
        if (chunk_started) {
          chat_append_message("chat",
            list(role = "assistant", content = ""),
            chunk = "end", session = session)
        }
        return("done")
      }
    }
    if (drain_done) break
  }
  "done"
})

stream_task <- ExtendedTask$new(function(user_input) {
  client$send(user_input)
  do_stream(client, interrupt_flag, session)
})

observeEvent(input$chat_user_input, {
  if (stream_task$status() == "running") return()
  interrupt_flag(FALSE)
  stream_task$invoke(input$chat_user_input)
})

# ESC key interrupt (requires JS: Shiny.setInputValue('esc', Math.random(), {priority:'event'}))
observeEvent(input$esc, {
  if (stream_task$status() == "running") interrupt_flag(TRUE)
})

See the Shiny Streaming article for a full walkthrough, or browse examples/13_shinychat_streaming.R directly.

Simple non-streaming pattern (receive_response_async)

For cases where streaming is not needed, receive_response_async() returns a promises::promise that resolves to the ResultMessage:

library(ClaudeAgentSDK)
library(promises)

client <- ClaudeSDKClient$new(ClaudeAgentOptions(max_turns = 1L))
client$connect()
client$send("Explain R in one sentence")

p <- client$receive_response_async(on_message = function(msg) {
  if (inherits(msg, "AssistantMessage")) {
    cat(msg$content[[1]]$text)
  }
})

# Drive the event loop (non-Shiny context)
result <- NULL
then(p, onFulfilled = function(val) result <<- val)
while (is.null(result)) later::run_now(timeoutSecs = 0.1)

cat("\nCost: $", result$total_cost_usd, "\n")
client$disconnect()

See examples/14_shinychat_simple.R for the Shiny ExtendedTask version.

Available Tools

See the Claude Code documentation for a complete list of available tools.

Development

Running Tests

# Unit tests (no CLI required)
devtools::test("/path/to/ClaudeAgentSDK")

# Or with testthat directly
testthat::test_dir("tests/testthat")

Development Setup

# Install git hooks (runs tests before push)
bash scripts/initial-setup.sh

Package Structure

R/
├── errors.R     # Error classes (_errors.py)
├── utils.R      # CLI discovery + buffer helpers
├── types.R      # All message/content S3 types (types.py)
├── options.R    # ClaudeAgentOptions() constructor
├── protocol.R   # JSON parser + message builders (message_parser.py)
├── transport.R  # SubprocessCLITransport R6 class
├── query.R      # claude_query() + claude_run()
├── client.R     # ClaudeSDKClient R6 class (client.py)
└── sessions.R   # list_sessions() etc. (sessions.py)

Package Checks

devtools::check("/path/to/ClaudeAgentSDK")

License and Terms

Use of this SDK is governed by Anthropic’s Commercial Terms of Service, 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.