Module 7: Integrations: Git, SDK, and Remote Control
Python and TypeScript SDK: programmatic access to Claude Code
Python and TypeScript SDK: programmatic access to Claude Code
Overview
So far you've interacted with Claude Code in exactly one way: you type a prompt in the terminal, Claude answers, you review, you repeat. That interactive model is powerful for development — but it has one fundamental limit: it needs a human in the loop.
What happens when you need Claude Code to review 200 files automatically? Or generate documentation every time someone pushes? Or validate code in a CI/CD pipeline with nobody watching? That's what the SDK is for.
The Claude Code SDK gives you programmatic access to the same capabilities you use interactively, but from Python or TypeScript code. Instead of typing a prompt in the terminal, you call a function in your script. Instead of reading the answer in the terminal, you get it back as a structured object your program can process.
What the Claude Code SDK is
The SDK (Software Development Kit) is a library that exposes Claude Code's capabilities through a programmatic API. It isn't the Anthropic API directly — it's a layer that runs Claude Code as a process and gives you control over its input and output.
SDK vs Anthropic API vs interactive Claude Code
| Feature | Interactive Claude Code | Claude Code SDK | Anthropic API |
|---|---|---|---|
| Interface | Terminal (manual prompt) | A function in your code | HTTP requests |
| Human required | Yes (you, in the terminal) | No (automatable) | No |
| Tool access | All (read, write, execute) | All (read, write, execute) | LLM only (no tools) |
| Project context | Yes (reads your codebase) | Yes (reads your codebase) | No (just the prompt) |
| CLAUDE.md | Yes (loaded automatically) | Yes (loaded automatically) | No |
| Skills and hooks | Yes | Yes | No |
| Use case | Interactive development | Automation and scripting | Apps and chatbots |
The key difference: the SDK runs the full Claude Code — with access to files, the terminal, Git, and everything you learned in earlier modules. The Anthropic API only gives you access to the language model, with no tools.
Python SDK
Installation
pip install claude-agent-sdk
A note on names: The Agent SDK package is
claude-agent-sdk(pip) /claude_agent_sdk(import). This is different from Anthropic's main SDK (pip install anthropic), which is used for direct API calls. Always check the current names in the official Agent SDK documentation, since Anthropic iterates fast.
Requirements:
- Python 3.8+
- Claude Code installed and authenticated (
claude --version) - Node.js 18+ (the SDK runs Claude Code as a Node process)
Basic use: the query() function
The simplest way to use the SDK is with query():
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, Message
async def main():
messages: list[Message] = []
async for message in query(
prompt="Explain this project's architecture",
options=ClaudeAgentOptions(max_turns=3)
):
if message.type == "text":
print(message.content)
asyncio.run(main())
query() is an async generator that yields messages. Each message can be of type text (Claude's answer), tool_use (Claude runs a tool), or tool_result (the tool's result).
Basic example: review a file
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def review_file(filepath: str) -> str:
result = []
async for message in query(
prompt=f"Review {filepath} for bugs, security issues, and code quality. Be concise.",
options=ClaudeAgentOptions(max_turns=5)
):
if message.type == "text":
result.append(message.content)
return "\n".join(result)
async def main():
review = await review_file("src/auth/login.py")
print(review)
asyncio.run(main())
Intermediate example: batch review of multiple files
import asyncio
import glob
from claude_agent_sdk import query, ClaudeAgentOptions
async def review_file(filepath: str) -> dict:
result = []
async for message in query(
prompt=f"""Review {filepath}. Return a brief summary with:
1. Purpose of the file
2. Any bugs or issues found
3. Suggestions for improvement
Be concise - max 10 lines.""",
options=ClaudeAgentOptions(max_turns=3)
):
if message.type == "text":
result.append(message.content)
return {
"file": filepath,
"review": "\n".join(result)
}
async def review_directory(directory: str) -> list[dict]:
python_files = glob.glob(f"{directory}/**/*.py", recursive=True)
tasks = [review_file(f) for f in python_files[:10]]
reviews = await asyncio.gather(*tasks)
return reviews
async def main():
reviews = await review_directory("src/")
for review in reviews:
print(f"\n{'='*60}")
print(f"File: {review['file']}")
print(f"{'='*60}")
print(review["review"])
asyncio.run(main())
This script reviews up to 10 Python files in parallel, each in its own Claude Code instance.
ClaudeAgentOptions: controlling execution
from claude_agent_sdk import ClaudeAgentOptions
options = ClaudeAgentOptions(
max_turns=10,
system_prompt="You are a senior code reviewer. Be thorough but concise.",
allowed_tools=["Read", "Grep", "Glob"],
model="sonnet",
cwd="/path/to/project"
)
| Parameter | Type | Description |
|---|---|---|
max_turns | int | Maximum conversation turns |
system_prompt | str | Additional system prompt |
allowed_tools | list[str] | Allowed tools |
model | str | Model to use (opus, sonnet) |
cwd | str | Working directory |
permission_mode | str | Permission mode |
Filtering message types
async for message in query(prompt="...", options=options):
match message.type:
case "text":
print(f"Claude says: {message.content}")
case "tool_use":
print(f"Claude uses: {message.tool} with {message.input}")
case "tool_result":
print(f"Result: {message.content[:100]}...")
TypeScript SDK
Installation
npm install @anthropic-ai/claude-agent-sdk
Requirements:
- Node.js 18+
- Claude Code installed and authenticated
Basic use: the query() function
import { query } from "@anthropic-ai/claude-agent-sdk";
async function main() {
for await (const message of query({
prompt: "Explain this project's architecture",
options: { maxTurns: 3 },
})) {
if (message.type === "text") {
console.log(message.content);
}
}
}
main();
Basic example: generate documentation
import { query } from "@anthropic-ai/claude-agent-sdk";
import { writeFile } from "fs/promises";
async function generateDocs(directory: string): Promise<string> {
const result: string[] = [];
for await (const message of query({
prompt: `Analyze the code in ${directory} and generate API documentation
in markdown format. Include: function signatures, parameters, return types,
and brief descriptions.`,
options: {
maxTurns: 10,
allowedTools: ["Read", "Grep", "Glob"],
},
})) {
if (message.type === "text") {
result.push(message.content);
}
}
return result.join("\n");
}
async function main() {
const docs = await generateDocs("src/routes");
await writeFile("docs/API.md", docs);
console.log("Documentation generated: docs/API.md");
}
main();
Intermediate example: a test generator
import { query } from "@anthropic-ai/claude-agent-sdk";
import { readdir, writeFile } from "fs/promises";
import { join, basename } from "path";
async function generateTestForFile(filepath: string): Promise<void> {
const result: string[] = [];
for await (const message of query({
prompt: `Read ${filepath} and generate comprehensive unit tests using pytest.
Cover: normal cases, edge cases, error handling.
Output ONLY the test code, nothing else.`,
options: {
maxTurns: 5,
allowedTools: ["Read", "Grep"],
},
})) {
if (message.type === "text") {
result.push(message.content);
}
}
const testFilename = `test_${basename(filepath)}`;
const testPath = join("tests", testFilename);
await writeFile(testPath, result.join("\n"));
console.log(`Generated: ${testPath}`);
}
async function main() {
const files = await readdir("src/services");
const pyFiles = files.filter((f) => f.endsWith(".py") && !f.startsWith("__"));
for (const file of pyFiles) {
await generateTestForFile(join("src/services", file));
}
}
main();
createSdkMcpServer: exposing Claude Code as an MCP server
The TypeScript SDK lets you expose Claude Code as an MCP server, so other agents or applications can use it as a tool:
import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
const server = createSdkMcpServer({
name: "claude-code-reviewer",
description: "Code review powered by Claude Code",
});
server.start();
This creates an MCP server that other clients can consume, delegating tasks to Claude Code programmatically.
SDK vs interactive Claude Code: when to use which
Use interactive Claude Code when:
- You're actively developing and need fast iteration
- The task needs your judgment at every step
- You're exploring a new codebase
- You need immediate visual feedback
Use the SDK when:
- The task is repetitive and predictable
- You need to process multiple files/repos
- The task runs with no human supervision
- You need to integrate Claude Code into another system
- The task is part of an automated pipeline
Decision table
| Scenario | Interactive | SDK |
|---|---|---|
| Implementing a new feature | ✅ | ❌ |
| Reviewing 50 code files | ❌ | ✅ |
| Real-time debugging | ✅ | ❌ |
| Code review in CI/CD | ❌ | ✅ |
| Generating docs for a whole project | ❌ | ✅ |
| Exploring an unfamiliar codebase | ✅ | ❌ |
| A pre-commit validation hook | ❌ | ✅ |
| Guided refactoring | ✅ | ❌ |
Practical SDK use cases
1. Automated code review in CI/CD
import asyncio
import subprocess
from claude_agent_sdk import query, ClaudeAgentOptions
async def review_pr_diff() -> str:
diff = subprocess.run(
["git", "diff", "main...HEAD"],
capture_output=True, text=True
).stdout
result = []
prompt = f"""Review this git diff for a PR. Focus on:
1. Bugs or logic errors
2. Security vulnerabilities
3. Performance issues
4. Code style violations
Diff:
{diff}
Format: bullet points, severity (HIGH/MEDIUM/LOW), file:line reference."""
async for message in query(
prompt=prompt,
options=ClaudeAgentOptions(
max_turns=3,
allowed_tools=["Read", "Grep"]
)
):
if message.type == "text":
result.append(message.content)
return "\n".join(result)
async def main():
review = await review_pr_diff()
print(review)
with open("pr_review.md", "w") as f:
f.write(review)
asyncio.run(main())
2. Documentation generator
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def generate_module_docs(module_path: str) -> str:
result = []
async for message in query(
prompt=f"""Analyze all files in {module_path} and generate comprehensive
documentation in markdown format. Include:
- Module overview
- All public functions/classes with signatures
- Parameters and return types
- Usage examples
- Dependencies""",
options=ClaudeAgentOptions(
max_turns=10,
allowed_tools=["Read", "Grep", "Glob"]
)
):
if message.type == "text":
result.append(message.content)
return "\n".join(result)
async def main():
modules = ["src/auth", "src/routes", "src/services"]
for module in modules:
docs = await generate_module_docs(module)
output = f"docs/{module.split('/')[-1]}.md"
with open(output, "w") as f:
f.write(docs)
print(f"Generated: {output}")
asyncio.run(main())
3. Batch code migrator
import asyncio
import glob
from claude_agent_sdk import query, ClaudeAgentOptions
async def migrate_file(filepath: str) -> dict:
result = []
async for message in query(
prompt=f"""Migrate {filepath} from Python 3.8 syntax to Python 3.12:
- Replace typing.Optional with X | None
- Replace typing.Union with X | Y
- Replace typing.List/Dict/Set with list/dict/set
- Use match/case where appropriate
Apply the changes directly to the file.""",
options=ClaudeAgentOptions(
max_turns=5,
allowed_tools=["Read", "Write"]
)
):
if message.type == "text":
result.append(message.content)
return {"file": filepath, "status": "migrated", "details": "\n".join(result)}
async def main():
files = glob.glob("src/**/*.py", recursive=True)
print(f"Migrating {len(files)} files...")
for f in files:
result = await migrate_file(f)
print(f" ✅ {result['file']}")
print("Migration complete.")
asyncio.run(main())
Comparisons and decisions
Python SDK vs TypeScript SDK
| Aspect | Python SDK | TypeScript SDK |
|---|---|---|
| Installation | pip install claude-agent-sdk | npm install @anthropic-ai/claude-agent-sdk (includes the Agent SDK) |
| API | query() async generator | query() async iterator |
| MCP server | Not available | createSdkMcpServer() |
| Ecosystem | Scripts, data pipelines, ML | Web apps, Node scripts, MCP |
| Concurrency | asyncio.gather() | Promise.all() |
| Best for | CI/CD, batch processing, data | MCP integration, web services |
SDK vs Headless CLI (-p)
| Aspect | SDK | Headless CLI |
|---|---|---|
| Complexity | Higher (Python/TS code) | Lower (one bash command) |
| Control | Fine-grained (streaming, types) | Basic (text/json output) |
| Error handling | Try/catch, types | Exit codes |
| Integration | Into any program | Into bash scripts |
| Best for | Complex logic, pipelines | Simple scripts, one-liners |
Rule of thumb: if you can solve it with a bash command, use the headless CLI (-p). If you need logic, parsing, or integration with another system, use the SDK.
Common patterns
Pattern: retry with backoff
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def query_with_retry(prompt: str, max_retries: int = 3) -> str:
for attempt in range(max_retries):
try:
result = []
async for message in query(
prompt=prompt,
options=ClaudeAgentOptions(max_turns=5)
):
if message.type == "text":
result.append(message.content)
return "\n".join(result)
except Exception as e:
if attempt == max_retries - 1:
raise
wait = 2 ** attempt
print(f"Attempt {attempt + 1} failed: {e}. Retrying in {wait}s...")
await asyncio.sleep(wait)
Pattern: restricting tools
review_options = ClaudeAgentOptions(
max_turns=5,
allowed_tools=["Read", "Grep", "Glob"]
)
modify_options = ClaudeAgentOptions(
max_turns=10,
allowed_tools=["Read", "Write", "Grep", "Glob"]
)
Use allowed_tools to control what Claude Code can do in each script. For review: read-only. For migrations: read and write.
Pattern: sequential processing with a summary
async def process_and_summarize(files: list[str]) -> str:
all_results = []
for f in files:
result = await review_file(f)
all_results.append(result)
summary_parts = []
async for message in query(
prompt=f"Summarize these code reviews into a single report:\n\n" +
"\n---\n".join([r["review"] for r in all_results]),
options=ClaudeAgentOptions(max_turns=3)
):
if message.type == "text":
summary_parts.append(message.content)
return "\n".join(summary_parts)
Pitfalls and edge cases
Pitfall 1: too many turns in batch processing
If you set max_turns=50 for a batch of 100 files, each file can burn a lot of tokens. Start with max_turns=3-5 for simple tasks.
Pitfall 2: excessive parallelism
Running 100 query() calls in parallel with asyncio.gather() can saturate your machine. Cap the concurrency:
import asyncio
semaphore = asyncio.Semaphore(5)
async def limited_review(filepath: str):
async with semaphore:
return await review_file(filepath)
tasks = [limited_review(f) for f in files]
results = await asyncio.gather(*tasks)
Pitfall 3: not validating the output
The SDK returns free-form text. If you need structured data, ask for JSON explicitly in the prompt and parse the result:
import json
async for message in query(
prompt="Analyze this file. Respond with JSON: {\"issues\": [...], \"score\": 0-10}",
options=options
):
if message.type == "text":
try:
data = json.loads(message.content)
except json.JSONDecodeError:
pass # Fallback: treat it as plain text
Pitfall 4: forgetting cwd
If your script runs from a directory other than the project's, Claude Code won't find the files:
options = ClaudeAgentOptions(
cwd="/absolute/path/to/your/project"
)
Pitfall 5: cost in batch processing
Every query() starts a full Claude Code session. For 100 files, that's 100 sessions. Monitor usage so you don't get surprised by the bill.
Complete worked example
A script that combines review, documentation, and reporting:
import asyncio
import glob
import json
from datetime import datetime
from claude_agent_sdk import query, ClaudeAgentOptions
REVIEW_OPTIONS = ClaudeAgentOptions(
max_turns=5,
allowed_tools=["Read", "Grep", "Glob"]
)
async def review_file(filepath: str) -> dict:
result = []
async for message in query(
prompt=f"""Review {filepath}. Respond with JSON only:
{{
"file": "{filepath}",
"issues": [
{{"severity": "HIGH|MEDIUM|LOW", "line": 0, "description": "..."}}
],
"quality_score": 0-10,
"summary": "one line summary"
}}""",
options=REVIEW_OPTIONS
):
if message.type == "text":
result.append(message.content)
text = "\n".join(result)
try:
return json.loads(text)
except json.JSONDecodeError:
return {"file": filepath, "issues": [], "quality_score": -1, "summary": text[:200]}
async def main():
files = glob.glob("src/**/*.py", recursive=True)
semaphore = asyncio.Semaphore(3)
async def limited(f):
async with semaphore:
return await review_file(f)
print(f"Reviewing {len(files)} files...")
reviews = await asyncio.gather(*[limited(f) for f in files])
high_issues = [
r for r in reviews
if any(i.get("severity") == "HIGH" for i in r.get("issues", []))
]
report = {
"date": datetime.now().isoformat(),
"total_files": len(files),
"files_with_high_issues": len(high_issues),
"average_quality": sum(
r.get("quality_score", 0) for r in reviews
if r.get("quality_score", -1) >= 0
) / max(len(reviews), 1),
"reviews": reviews
}
with open("code_review_report.json", "w") as f:
json.dump(report, f, indent=2)
print(f"\nReport saved: code_review_report.json")
print(f"Files reviewed: {report['total_files']}")
print(f"High-severity issues: {report['files_with_high_issues']}")
print(f"Average quality: {report['average_quality']:.1f}/10")
asyncio.run(main())
Practice exercises
Exercise 1: Basic — Your first SDK script
Write a Python script that uses the SDK to analyze your project's structure and produce a summary.
Requirements:
- Install
claude-agent-sdk - Use
query()with a prompt that asks for the analysis - Print the result to the console
- Cap it at 3 turns
Solution
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def analyze_project():
result = []
async for message in query(
prompt="Analyze this project's structure. List: main language, "
"framework, directory structure, and entry points. Be concise.",
options=ClaudeAgentOptions(max_turns=3)
):
if message.type == "text":
result.append(message.content)
print("\n".join(result))
asyncio.run(analyze_project())
Run it: python analyze.py from the root of your project.
Exercise 2: Intermediate — Reviewing a directory
Write a script that reviews every file in a directory and produces a markdown report.
Requirements:
- Take the directory as an argument
- Review each file (cap it at 5 so you don't burn too many tokens)
- Produce a
review-report.mdfile with the results - Include a quality score per file
Solution
import asyncio
import glob
import sys
from claude_agent_sdk import query, ClaudeAgentOptions
async def review_file(filepath: str) -> dict:
result = []
async for message in query(
prompt=f"Review {filepath}. Give: 1) quality score (1-10), "
f"2) top 3 issues if any, 3) one-line summary.",
options=ClaudeAgentOptions(
max_turns=3,
allowed_tools=["Read"]
)
):
if message.type == "text":
result.append(message.content)
return {"file": filepath, "review": "\n".join(result)}
async def main():
directory = sys.argv[1] if len(sys.argv) > 1 else "src"
files = glob.glob(f"{directory}/**/*.py", recursive=True)[:5]
print(f"Reviewing {len(files)} files in {directory}...")
reviews = []
for f in files:
review = await review_file(f)
reviews.append(review)
print(f" Done: {f}")
with open("review-report.md", "w") as out:
out.write("# Code Review Report\n\n")
for r in reviews:
out.write(f"## {r['file']}\n\n")
out.write(f"{r['review']}\n\n---\n\n")
print(f"\nReport saved: review-report.md")
asyncio.run(main())
Exercise 3: Intermediate — A test generator with TypeScript
Write a TypeScript script that generates tests for the files in a directory.
Requirements:
- Read
.tsfiles from a directory - For each file, generate tests with the SDK
- Save the tests in a
tests/directory - Use
allowedTools: ["Read"](source code, read-only)
Solution
import { query } from "@anthropic-ai/claude-agent-sdk";
import { readdir, writeFile, mkdir } from "fs/promises";
import { join, basename } from "path";
async function generateTest(filepath: string): Promise<string> {
const result: string[] = [];
for await (const message of query({
prompt: `Read ${filepath} and generate unit tests using vitest.
Cover normal cases, edge cases, and error handling.
Output ONLY the test code.`,
options: {
maxTurns: 5,
allowedTools: ["Read"],
},
})) {
if (message.type === "text") {
result.push(message.content);
}
}
return result.join("\n");
}
async function main() {
const dir = process.argv[2] || "src";
const files = await readdir(dir);
const tsFiles = files.filter(
(f) => f.endsWith(".ts") && !f.endsWith(".test.ts")
);
await mkdir("tests", { recursive: true });
for (const file of tsFiles.slice(0, 5)) {
const filepath = join(dir, file);
console.log(`Generating test for ${filepath}...`);
const testCode = await generateTest(filepath);
const testFile = join("tests", `${basename(file, ".ts")}.test.ts`);
await writeFile(testFile, testCode);
console.log(` Created: ${testFile}`);
}
}
main();
Exercise 4: Advanced — A CI/CD review bot
Write a script you can run in CI/CD to review a PR's changes and leave a comment.
Requirements:
- Read the diff with
git diff main...HEAD - Use the SDK to review the diff
- Produce a
pr-review.mdfile with the result - The result must be formatted so you can paste it into a GitHub comment
Solution
import asyncio
import subprocess
from claude_agent_sdk import query, ClaudeAgentOptions
async def review_pr():
diff = subprocess.run(
["git", "diff", "main...HEAD"],
capture_output=True, text=True
).stdout
if not diff.strip():
print("No changes to review.")
return
result = []
async for message in query(
prompt=f"""You are a code reviewer. Review this PR diff and provide
feedback formatted for a GitHub PR comment.
Use this format:
## Code Review Summary
[1-2 sentence overview]
## Issues Found
- 🔴 **Critical**: [description] (`file:line`)
- 🟡 **Warning**: [description] (`file:line`)
- 🔵 **Suggestion**: [description] (`file:line`)
## What's Good
- [positive observations]
Diff:
{diff[:10000]}""",
options=ClaudeAgentOptions(
max_turns=3,
allowed_tools=["Read", "Grep"]
)
):
if message.type == "text":
result.append(message.content)
review_text = "\n".join(result)
with open("pr-review.md", "w") as f:
f.write(review_text)
print(review_text)
asyncio.run(review_pr())
Exercise 5: Challenge — A complete pipeline
Combine review + docs + tests into a single script that processes a whole module.
Requirements:
- Take a module path as an argument
- Step 1: Review every file
- Step 2: Generate the module's documentation
- Step 3: Find files without tests and generate basic tests for them
- Output: a
reports/directory with review.md, docs.md, and the generated tests
Guide
The script's structure:
async def main():
module = sys.argv[1]
# Step 1: Review
reviews = await review_all_files(module)
save_markdown("reports/review.md", reviews)
# Step 2: Documentation
docs = await generate_docs(module)
save_markdown("reports/docs.md", docs)
# Step 3: Generate missing tests
untested = find_files_without_tests(module)
for f in untested:
test = await generate_test(f)
save_file(f"tests/test_{basename(f)}", test)
print("Pipeline complete. Check reports/ directory.")
Use the functions from the earlier exercises as your base. Cap concurrency with asyncio.Semaphore(3) so you don't saturate the machine.
Summary
What you learned in this capsule:
- The Claude Code SDK gives programmatic access to every Claude Code capability
- Python Agent SDK (
claude-agent-sdk): install with pip,query()as an async generator - TypeScript SDK (
@anthropic-ai/claude-agent-sdk): install with npm,query()as an async iterator,createSdkMcpServer()to expose Claude Code as an MCP server - SDK vs interactive: interactive for development, SDK for automation and batch processing
- SDK vs headless CLI: headless for simple scripts, SDK for complex logic and error handling
- Practical use cases: code review in CI/CD, docs generation, batch migration, testing
- Pitfalls: control turns, cap concurrency, validate output, set cwd, monitor cost
Next capsule: 04 - Headless mode CLI — the -p flag to run Claude Code from bash scripts with no interaction.
Additional resources
Official documentation
- Claude Code SDK — Full SDK documentation
- Python Agent SDK Reference — The package on PyPI
- TypeScript SDK Reference — The package on npm
- SDK Examples — Official examples
Integrations
- GitHub Actions + SDK — Using the SDK in CI/CD
- MCP + SDK — Exposing Claude Code as an MCP server
- Headless Mode — The CLI alternative to the SDK
Complementary
- Best Practices — Good practice for automation
- CLI Reference — Complete CLI reference