Claude Code SDK is pretty wild
Mixing claude with procedural code beyond one-shot i/o.

I’ve been using claude code on and off since it launched a year ago. Since the Christmas holiday, it seems a ton of folks outside the software industry have discovered it as well.
Despite its popularity, I’ve seen almost no discussion of Claude Code SDK. It’s essentially a typescript or python wrapper to the agent harness that allows you to fully manage the handoff between agentic and procedural parts of an application.
Beyond --print mode
If you’ve discovered the --print flag, you’ve almost certainly scripted up some terrible nonsense to run it as a subprocess.
Stop that right now!
The SDK documentation is dense. It does not take the time to explain the basics of the turn loop or tool hooks. I suspect that most people who have attempted to use the SDK have done so in a one-shot sort of way, by giving a prompt and letting Claude cook until it yields.
Control handoff and tool use
Claude code is just a while loop.
until llm says stop:
give llm some inputs
do whatever it says (read file, run bash)
When you build an agent SKILL.md, you hand-wavishly describe how to use an API or function or script. Claude reads those instructions and then uses its existing tools (read/write/bash/grep) to do whatever the skill describes.
Claude is like the little dude inside the big dude’s head
– Will Smith

When you build custom tools with the SDK, you must implement an MCP tool interface, run it synchronously in-process, and hand the output back for the next turn.
A Toy Example
The official docs provide this example. It’s a bit unsatisfying because it hides the details, so let’s reimplement without the automagical tool_runner.
from anthropic import Anthropic
import json
client = Anthropic()
max_turns = 10
SYSTEM_PROMPT = "System Prompt Here"
MESSAGES = [{"role": "user", "content": [{"type": "text", "text": "Tell me the weather in San Francisco." }]}]
# This is the MCP spec!
TOOLS = [{
"name": "get_weather",
"description": "Get the current weather in a given location",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
}
},
"required": ["location"]
}
}]
def get_weather(location: str) -> str:
return json.dumps(
{
"location": location,
"temperature": "68°F",
"condition": "Sunny",
}
)
for _ in range(max_turns):
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
system=SYSTEM_PROMPT,
tools=TOOLS,
messages=MESSAGES,
)
if response.stop_reason == "tool_use":
tool_results = []
assistant_content = []
for block in response.content:
assistant_content.append(block)
# Do what claude says
if block.type == "tool_use":
if block.name == "get_weather":
result = get_weather(block.input.get("location"))
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
# Append the request and response to the session log:
# Claude says "Please run this tool"
messages.append({"role": "assistant", "content": assistant_content})
# User says "Here you go"
messages.append({"role": "user", "content": tool_results})
elif response.stop_reason == "end_turn":
print("Claude exited.")
You’d miss it in the beta.tool_runner wrapper, but tool use blocks append to an ever-growing messages array that gets fed back into the client every turn.
Thats exactly how claude code works, and its why the SDK token use grows exponentially by turn.
A working project
If you don’t mind the mess, here’s a real project for updating a Google Calendar based on the posts from an RSS stream.
When a post announces an upcoming event, the agent has 4 tools:
get_imagessearch_events_by_datesearch_events_by_keywordsubmit_decision
Takeaway
Using the same harness framework and a completely different set of tools, claude code can do tasks entirely unrelated to writing code.