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

FeatureInteractive Claude CodeClaude Code SDKAnthropic API
InterfaceTerminal (manual prompt)A function in your codeHTTP requests
Human requiredYes (you, in the terminal)No (automatable)No
Tool accessAll (read, write, execute)All (read, write, execute)LLM only (no tools)
Project contextYes (reads your codebase)Yes (reads your codebase)No (just the prompt)
CLAUDE.mdYes (loaded automatically)Yes (loaded automatically)No
Skills and hooksYesYesNo
Use caseInteractive developmentAutomation and scriptingApps 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"
)
ParameterTypeDescription
max_turnsintMaximum conversation turns
system_promptstrAdditional system prompt
allowed_toolslist[str]Allowed tools
modelstrModel to use (opus, sonnet)
cwdstrWorking directory
permission_modestrPermission 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

ScenarioInteractiveSDK
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

AspectPython SDKTypeScript SDK
Installationpip install claude-agent-sdknpm install @anthropic-ai/claude-agent-sdk (includes the Agent SDK)
APIquery() async generatorquery() async iterator
MCP serverNot availablecreateSdkMcpServer()
EcosystemScripts, data pipelines, MLWeb apps, Node scripts, MCP
Concurrencyasyncio.gather()Promise.all()
Best forCI/CD, batch processing, dataMCP integration, web services

SDK vs Headless CLI (-p)

AspectSDKHeadless CLI
ComplexityHigher (Python/TS code)Lower (one bash command)
ControlFine-grained (streaming, types)Basic (text/json output)
Error handlingTry/catch, typesExit codes
IntegrationInto any programInto bash scripts
Best forComplex logic, pipelinesSimple 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.md file 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 .ts files 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.md file 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

Integrations

Complementary