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:
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
Recommended: coro::async + poll_messages()
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")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)
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.