
Introduction
Duct-taping scripts, tools and prompts together gets old quickly. The Claude Agent SDK turns Claude Code’s “plan → build → run” workflows into real, programmable agents — automating tasks, wiring up tools and shipping command-line applications without piles of glue code. For anyone who already likes using Claude in the terminal, the SDK offers the same experience with proper structure, state and extensibility. Built on the foundation of Claude Code, it lets developers integrate Claude with a local environment, give it access to tools, and orchestrate complex workflows spanning coding, research, note-taking and automation.
This tutorial installs the Claude Agent SDK and builds a small multi-tool CLI that chains steps from start to finish.
Installing the Claude Agent SDK
1. Prerequisites
Python 3.10 or higher, Node.js 18+ for the CLI, and a Claude API key or Anthropic account.
2. Install the Claude Code CLI
On Windows, install the Claude Code CLI from PowerShell:
irm https://claude.ai/install.ps1 | iexAdd the path to the system environment, restart PowerShell and test. On other platforms, npm works:
npm i -g @anthropic-ai/claude-codeAfter installation, typing claude signs in from the terminal.
3. Install the Claude Agent SDK (Python)
pip install claude-agent-sdkA CLINotFoundError at this stage usually means the Claude CLI is not installed correctly or missing from PATH.
Building a multi-tool app with the Claude Agent SDK
The example project, TrendSmith, tracks live market trends across industries including startups, AI, finance and sustainability. It combines Claude Sonnet 4.5, web search, web fetch and local storage tools in a single agent system. All code goes into a file called trend_smith.py.
1. Imports and basic settings
The first block loads Python libraries, the Claude Agent SDK types, a small help menu, model names, and soft gray text styles for status lines.
import asyncio
import os
import re
import sys
import time
from datetime import datetime
from pathlib import Path
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ClaudeSDKClient,
ResultMessage,
TextBlock,
ToolResultBlock,
ToolUseBlock,
)
HELP = """Commands:
/trend Quick multi-source scan (auto-saves markdown)
/scan Short one-page scan
/help /exit Help / Quit
"""
MODEL = os.getenv("CLAUDE_MODEL", "sonnet") # e.g. "sonnet-4.5"
GRAY = " 33(90m"
RESET = " 33(0m" 2. System prompt and report destination
Next come the “house rules” for answers — fast, concise, coherent sections — and a reports/ folder beside the script where saved briefs will land.
SYS = """You are TrendSmith, a fast, concise trend researcher.
- Finish quickly (~20 s).
- For /trend: ≤1 WebSearch + ≤2 WebFetch from distinct domains.
- For /scan: ≤1 WebFetch only.
Return for /trend:
TL;DR (1 line)
3-5 Signals (short bullets)
Key Players, Risks, 30/90-day Watchlist
Sources (markdown: **Title** -- URL)
Return for /scan: 5 bullets + TL;DR + Sources.
After finishing /trend, the client will auto-save your full brief.
"""
BASE = Path(__file__).parent
REPORTS = BASE / "reports"3. Saving files securely
These helpers create safe file names, build folders when necessary, and fall back to the home directory so reports can always be saved.
def _ts():
return datetime.now().strftime("%Y%m%d_%H%M")
def _sanitize(s: str):
return re.sub(r"(^w-.)+", "_", s).strip("_") or "untitled"
def _ensure_dir(p: Path):
try:
p.mkdir(parents=True, exist_ok=True)
except Exception:
pass
def _safe_write(path: Path, text: str) -> Path:
"""Write text to path; if directory/permission fails, fall back to ~/TrendSmith/reports."""
try:
_ensure_dir(path.parent)
path.write_text(text, encoding="utf-8")
return path
except Exception:
home_reports = Path.home() / "TrendSmith"https://www.kdnuggets.com/"reports"
_ensure_dir(home_reports)
fb = home_reports / path.name
fb.write_text(text, encoding="utf-8")
return fb
def save_report(topic: str, text: str) -> Path:
filename = f"{_sanitize(topic)}_{_ts()}.md"
target = REPORTS / filename
return _safe_write(target, text.strip() + "n")4. Tracking each run
A small state object keeps what one request needs — streamed text, model, tool count, token usage and timing — then resets cleanly before the next request.
class State:
def __init__(self):
self.brief = ""
self.model_raw = None
self.usage = {}
self.cost = None
self.last_cmd = None
self.last_topic = None
self.tools = {}
self.t0 = 0.0
self.t1 = 0.0
def reset(self):
self.brief = ""
self.model_raw = None
self.usage = {}
self.cost = None
self.tools = {}
self.t0 = time.perf_counter()
self.t1 = 0.0
def friendly_model(name: str | None) -> str:
if not name:
return MODEL
n = (name or "").lower()
if "sonnet-4-5" in n or "sonnet_4_5" in n:
return "Claude 4.5 Sonnet"
if "sonnet" in n:
return "Claude Sonnet"
if "haiku" in n:
return "Claude Haiku"
if "opus" in n:
return "Claude Opus"
return name or "Unknown"5. Run summary
A clean gray box prints model, tokens, tool usage and duration without blending into the streamed content.
def usage_footer(st: State, opts_model: str):
st.t1 = st.t1 or time.perf_counter()
dur = st.t1 - st.t0
usage = st.usage or {}
it = usage.get("input_tokens")
ot = usage.get("output_tokens")
total = usage.get("total_tokens")
if total is None and (it is not None or ot is not None):
total = (it or 0) + (ot or 0)
tools_used = ", ".join(f"{k}×{v}" for k, v in st.tools.items()) or "--"
model_label = friendly_model(st.model_raw or opts_model)
box = (
"┌─ Run Summary ─────────────────────────────────────────────",
f"│ Model: {model_label}",
f"│ Tokens: {total if total is not None else '?'}"
+ (f" (in={it if it is not None else '?'} | out={ot if ot is not None else '?'})"
if (it is not None or ot is not None) else ""),
f"│ Tools: {tools_used}",
f"│ Duration: {dur:.1f}s",
"└───────────────────────────────────────────────────────────",
)
print(GRAY + "n".join(box) + RESET, file=sys.stderr)6. Main loop
The main loop starts the app, reads a command, queries the model, streams the answer, saves /trend reports and prints summaries.
async def main():
"""Setup → REPL → parse → query/stream → auto-save → summary."""
st = State()
_ensure_dir(REPORTS)
opts = ClaudeAgentOptions(
model=MODEL,
system_prompt=SYS,
allowed_tools=("WebFetch", "WebSearch"),
)
print("📈 TrendSmith nn" + HELP)
async with ClaudeSDKClient(options=opts) as client:
while True:
# Read input
try:
user = input("nYou: ").strip()
except (EOFError, KeyboardInterrupt):
print("nBye!")
break
if not user:
continue
low = user.lower()
# Basic commands
if low in {"/exit", "exit", "quit"}:
print("Bye!")
break
if low in {"/help", "help"}:
print(HELP)
continue
# Parse into a prompt
if low.startswith("/trend "):
topic = user.split(" ", 1)(1).strip().strip('"')
if not topic:
print('e.g. /trend "AI chip startups"')
continue
st.last_cmd, st.last_topic = "trend", topic
prompt = f"Run a fast trend scan for '{topic}' following the output spec."
elif low.startswith("/scan "):
q = user.split(" ", 1)(1).strip()
if not q:
print('e.g. /scan "AI hardware news"')
continue
st.last_cmd, st.last_topic = "scan", q
prompt = f"Quick scan for '{q}' in under 10s (≤1 WebFetch). Return 5 bullets + TL;DR + sources."
else:
st.last_cmd, st.last_topic = "free", None
prompt = user
# Execute request and stream results
st.reset()
print(f"{GRAY}▶ Working...{RESET}")
try:
await client.query(prompt)
except Exception as e:
print(f"{GRAY}❌ Query error: {e}{RESET}")
continue
try:
async for m in client.receive_response():
if isinstance(m, AssistantMessage):
st.model_raw = st.model_raw or m.model
for b in m.content:
if isinstance(b, TextBlock):
st.brief += b.text or ""
print(b.text or "", end="")
elif isinstance(b, ToolUseBlock):
name = b.name or "Tool"
st.tools(name) = st.tools.get(name, 0) + 1
print(f"{GRAY}n🛠 Tool: {name}{RESET}")
elif isinstance(b, ToolResultBlock):
pass # quiet tool payloads
elif isinstance(m, ResultMessage):
st.usage = m.usage or {}
st.cost = m.total_cost_usd
except Exception as e:
print(f"{GRAY}n⚠ Stream error: {e}{RESET}")
# Auto-save trend briefs and show the summary
if st.last_cmd == "trend" and st.brief.strip():
try:
saved_path = save_report(st.last_topic or "trend", st.brief)
print(f"n{GRAY}✅ Auto-saved → {saved_path}{RESET}")
except Exception as e:
print(f"{GRAY}⚠ Save error: {e}{RESET}")
st.t1 = time.perf_counter()
usage_footer(st, opts.model)
if __name__ == "__main__":
asyncio.run(main())Testing the TrendSmith app
Running the Python file starts the CLI. The commands: /trend "topic" runs a brief multi-source scan and saves it automatically to reports/*.md; /scan "topic" produces a one-page quick scan (at most one WebFetch) and prints it without saving; /help shows the commands; /exit quits.
![]()
![]()
Testing the /trend option on AI chip startups:
/trend "AI chip startups"The app used several search and web-scraping tools to gather information from different websites.
![]()
![]()
It streamed the answer, saved the report to a Markdown file and generated a usage summary — at a cost of $0.136 for the run.
![]()
![]()
A preview of the saved Markdown report on AI chip startups:
![]()
![]()
The scan option generates a concise summary using a simple web search and fetch:
![]()
![]()
Final thoughts
The app runs smoothly, and for anyone already on a Claude Code plan the SDK is a natural way to turn daily terminal workflows into a reliable, repeatable agentic CLI. Good uses include automating common development tasks (debugging, testing, deployment), scripting analysis or ops routines, and packaging flows into reusable, shareable tools. The SDK suits professionals who want stability, reproducibility and low glue-code overhead — and Claude Code itself can help write agentic applications with the SDK.
Limitations and what to watch
The SDK requires a paid Claude API key or subscription, and costs scale with tool calls and tokens — the $0.136 sample run shows the order of magnitude, but heavy multi-tool research runs cost more. The SDK and CLI evolve quickly, so commands and type names may shift between versions; the official documentation is the reference. For a contrasting approach that runs entirely offline on open models, see the fully local multi-agent orchestration tutorial, and for the wider tooling landscape, 10 GitHub repositories to master vibe coding.