<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nikita Sinenko</title>
    <description>The latest articles on DEV Community by Nikita Sinenko (@nsinenko).</description>
    <link>https://dev.to/nsinenko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F27402%2Fb02bc596-37c4-4eb5-a104-319f43449688.jpg</url>
      <title>DEV Community: Nikita Sinenko</title>
      <link>https://dev.to/nsinenko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nsinenko"/>
    <language>en</language>
    <item>
      <title>Build Claude agents using Ruby</title>
      <dc:creator>Nikita Sinenko</dc:creator>
      <pubDate>Fri, 03 Jul 2026 08:58:17 +0000</pubDate>
      <link>https://dev.to/nsinenko/is-there-a-claude-agent-sdk-for-ruby-heres-what-to-use-instead-42jl</link>
      <guid>https://dev.to/nsinenko/is-there-a-claude-agent-sdk-for-ruby-heres-what-to-use-instead-42jl</guid>
      <description>&lt;p&gt;I went looking for a Ruby port of Anthropic's Claude Agent SDK this week, the thing that ships as &lt;code&gt;pip install claude-agent-sdk&lt;/code&gt; for Python and &lt;code&gt;npm install @anthropic-ai/claude-agent-sdk&lt;/code&gt; for TypeScript. There is no gem in that list: Ruby has no official Claude Agent SDK, and one isn't in the works as far as any changelog shows.&lt;/p&gt;

&lt;p&gt;That's a different gap than it sounds like at first. Ruby already has an official Anthropic library, the &lt;code&gt;anthropic&lt;/code&gt; gem at &lt;code&gt;anthropics/anthropic-sdk-ruby&lt;/code&gt;, but that one is the Client SDK: the plain API wrapper where you write the tool loop yourself. The Agent SDK is a different product: Claude Code's actual harness (agent loop, filesystem tools, permissions, hooks, subagents, MCP support) packaged as a library. Ruby has the first one and not the second, and most of the confusion I saw searching for this comes from tutorials that treat them as the same thing. Some of it is also naming: Anthropic called this the Claude Code SDK before renaming it the Claude Agent SDK, so "claude code sdk ruby" searches land in the same gap.&lt;/p&gt;

&lt;p&gt;So "wait for the SDK" was the wrong question. The real one is which of three paths gets an agent running in Ruby with the least friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 1: the official gem, hand-rolled loop
&lt;/h2&gt;

&lt;p&gt;This is what I reach for when the agent needs to act on my own app, not on a filesystem or a shell. Look up an invoice and draft a reply to it, that kind of thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Anthropic::AgentRunner&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;client: &lt;/span&gt;&lt;span class="no"&gt;Anthropic&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
    &lt;span class="vi"&gt;@tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;max_turns: &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;max_tokens: &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;messages: &lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;tools: &lt;/span&gt;&lt;span class="vi"&gt;@tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:tool_use&lt;/span&gt;

      &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;run_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"agent did not converge in &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; turns"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:tool_use&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"tool_result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;tool_use_id: &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;max_turns&lt;/code&gt; cap is there because a confused agent that can loop forever will happily bill forever, and I would rather it raise loudly than retry the same tool call forty times in the background.&lt;/p&gt;

&lt;p&gt;If you don't want to own the loop, the &lt;code&gt;anthropic&lt;/code&gt; gem also ships a &lt;code&gt;tool_runner&lt;/code&gt; (under the &lt;code&gt;beta.messages&lt;/code&gt; namespace for now) that does the same thing for you, call the model, run the tool, feed the result back, repeat until the model stops asking. I write the loop by hand when I need a checkpoint mid-turn (human approval before a tool that sends an email, say), and use &lt;code&gt;tool_runner&lt;/code&gt; otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 2: the unofficial claude-agent-sdk gem
&lt;/h2&gt;

&lt;p&gt;If what you actually want is the Claude Code harness itself in Ruby, there's a community gem for that: &lt;a href="https://github.com/ya-luotao/claude-agent-sdk-ruby" rel="noopener noreferrer"&gt;&lt;code&gt;ya-luotao/claude-agent-sdk-ruby&lt;/code&gt;&lt;/a&gt;, published as &lt;code&gt;claude-agent-sdk&lt;/code&gt;. It deliberately mirrors the Python SDK's shape: &lt;code&gt;ClaudeAgentSDK.query&lt;/code&gt; for one-off calls, &lt;code&gt;ClaudeAgentSDK::Client&lt;/code&gt; for streaming sessions, &lt;code&gt;ClaudeAgentSDK.create_tool&lt;/code&gt; for in-process tools.&lt;/p&gt;

&lt;p&gt;One mechanical detail before you add it to a Gemfile: it doesn't call the Anthropic API directly. It shells out to the &lt;code&gt;claude&lt;/code&gt; CLI as a subprocess and talks stream-JSON over stdin/stdout, the same protocol the official SDKs use internally. Which means your Ruby app now has a Node.js and CLI dependency everywhere it runs, CI included, and you're installing &lt;code&gt;@anthropic-ai/claude-code&lt;/code&gt; alongside your gems.&lt;/p&gt;

&lt;p&gt;It's also young: v0.19.0 as I write this, 42 stars, one maintainer, already on its 38th release. That pace is a good sign for a side project and a reason to pin the exact version if it's going anywhere load-bearing. The README says plainly it isn't affiliated with Anthropic, which is worth taking at face value. I compare all three paths in more depth, including how each one reaches Bedrock and Vertex, in &lt;a href="https://nsinenko.com/claude-agent-sdk-ruby/" rel="noopener noreferrer"&gt;the longer Claude Agent SDK in Ruby comparison&lt;/a&gt; on my site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 3: shell out to the Claude Code CLI
&lt;/h2&gt;

&lt;p&gt;The plainest option, and sometimes the right one for CI scripts or a one-off automation: run &lt;code&gt;claude -p "your prompt" --output-format json&lt;/code&gt; as a subprocess and parse the JSON that comes back, with no gem in the middle. (Plain &lt;code&gt;claude -p&lt;/code&gt; prints text; the flag is what makes the output parseable.) It's what the community gem is doing under the hood anyway, minus the Ruby object model wrapped around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one
&lt;/h2&gt;

&lt;p&gt;If the agent's job is your database and your business logic, use the plain &lt;code&gt;anthropic&lt;/code&gt; gem. The harness you actually want is your own Rails app with its existing authorization, not a generic filesystem-and-shell harness built for something else.&lt;/p&gt;

&lt;p&gt;If the agent's job is files and a shell, a coding assistant, a refactor bot, a CI reviewer, that's what the Claude Code harness is built for, and the community gem or the raw CLI gets you there. I wouldn't build my own version of that harness from the plain API gem; that's reinventing a lot of surface area someone else already built and is actively maintaining.&lt;/p&gt;

&lt;p&gt;If you're building agents in Rails more broadly, I also have a &lt;a href="https://nsinenko.com/rails-ai-agents/" rel="noopener noreferrer"&gt;walkthrough of wiring a Claude agent into Rails background jobs and Turbo Streams&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>ai</category>
      <category>claude</category>
    </item>
  </channel>
</rss>
