n4nAI

Building a custom coding agent with the Claude Agent SDK

Build a custom coding agent with the Claude Agent SDK. Hands-on tutorial covering setup, tool config, system prompts, and running real repo tasks.

n4n Team2 min read513 words

Audio narration

Coming soon — every post will get a voice note here.

A claude agent sdk coding agent gives you programmatic control over Claude’s ability to read, write, and execute code inside a repository. In this tutorial we’ll stand up a minimal but real agent that scaffolds a Node.js service, runs its tests, and reports back—without the Claude Code CLI. You’ll need the official SDK, an API key, and about twenty minutes.

Prerequisites

  • Node.js 20+ (the SDK uses native async iterators)
  • An Anthropic API key exported as ANTHROPIC_API_KEY
  • A scratch Git repo to experiment in
  • TypeScript 5+ and tsx for running the script

Install the SDK:

npm init -y
npm install @anthropic-ai/claude-code zod
npm install -D tsx typescript

The @anthropic-ai/claude-code package exposes a query function that streams agent messages. That is the entire surface we need to build a claude agent sdk coding agent.

Step 1: A bare agent loop

Create agent.ts. The simplest possible run looks like this:

import { query } from "@anthropic-ai/claude-code";

async function run(prompt: string) {
  for await (const msg of query({
    prompt,
    options: {
      allowedTools: ["Read", "Write", "Bash"],
      model: "claude-opus-4-20250514",
    },
  })) {
    if (msg.type === "assistant") {
      process.stdout.write(msg.message.content[0]?.text ?? "");
    }
  }
}

run("Create a file hello.ts that exports a function greeting(name: string).");

Run it:

npx tsx agent.ts

Expected output (truncated):

I'll create hello.ts.
[Write] hello.ts
File written.

The agent created hello.ts in the current directory. The allowedTools array restricts what it can do; omit Bash and it cannot run anything.

Step 2: Pin the working directory and system prompt

Blind file writes are dangerous. Constrain the agent to a sandbox/ folder and give it a persona. The SDK accepts systemPrompt and cwd:

import { mkdirSync } from "node:fs";
import { query } from "@anthropic-ai/claude-code";

mkdirSync("sandbox", { recursive: true });

async function run(task: string) {
  for await (const msg of query({
    prompt: task,
    options: {
      allowedTools: ["Read", "Write", "Bash"],
      model: "claude-opus-4-20250514",
      cwd: "sandbox",
      systemPrompt: [
        "You are a senior backend engineer.",
        "Write only TypeScript. Never use any dependencies not in package.json.",
        "After writing code, run `npm test` and fix failures.",
      ].join("\n"),
    },
  })) {
    if (msg.type === "result") {
      console.log("\nEXIT:", msg.subtype);
    } else if (msg.type === "assistant") {
      console.log(msg.message.content.map(c => c.text ?? "").join(""));
    }
  }
}

Now the claude agent sdk coding agent operates inside sandbox/ and self-verifies.

Step 3: Capture tool calls for logging

In a real system you want an audit trail. The stream emits tool_use and tool_result blocks inside assistant and user messages. Extract them:

function logToolUse(msg: any) {
  for (const block of msg.message.content) {
    if (block.type === "tool_use") {
      console.log(`> tool=${block.name} input=${JSON.stringify(block.input)}`);
    }
  }
}

Wire it into the loop:

if (msg.type === "assistant") {
  logToolUse(msg);
  console.log(msg.message.content.map(c => c.text ?? "").join(""));
}

You’ll see lines like:

> tool=Write input={"file_path":"sandbox/index.ts","content":"..."}
> tool=Bash input={"command":"npm test"}

Step 4: Drive a concrete task

Let’s make the agent build a tiny HTTP server with one test. Pass a precise spec:

await run(`
Create package.json with typescript and vitest.
Create src/server.ts exporting start(port) that returns a http.Server on GET /health returning 200.
Create src/server.test.ts that requests /health and asserts status 200.
Run the test.
`);

The agent will:

  1. Write package.json
  2. Write src/server.ts
  3. Write src/server.test.ts
  4. Run npm install && npm test
  5. Loop on any type errors

Checkpoint output after a successful run:

> tool=Bash input={"command":"npm test"}
PASS src/server.test.ts

If you inspect sandbox/, the files exist and are runnable.

Step 5: Make the agent reusable as a module

Wrap the loop in a class so you can call it from a larger app:

import { query } from "@anthropic-ai/claude-code";

export class CodingAgent {
  constructor(private cwd: string, private model = "claude-opus-4-20250514") {}

  async execute(task: string): Promise<string> {
    let last = "";
    for await (const msg of query({
      prompt: task,
      options: {
        allowedTools: ["Read", "Write", "Bash"],
        model: this.model,
        cwd: this.cwd,
        systemPrompt: "You write TypeScript and verify with npm test.",
      },
    })) {
      if (msg.type === "result") last = msg.subtype;
      if (msg.type === "assistant")
        last = msg.message.content.map(c => c.text ?? "").join("");
    }
    return last;
  }
}

This claude agent sdk coding agent is now embeddable in a CI step or a chat backend.

Step 6: Routing through an inference gateway

The SDK points at Anthropic by default. If you want automatic fallback when a provider is rate-limited, you can proxy the underlying requests through an OpenAI-compatible gateway. n4n.ai exposes one endpoint that addresses 240+ models and honors client routing directives, but the Claude Agent SDK currently expects Anthropic’s wire format, so you’d need a shim that translates. For most teams, direct Anthropic access is simpler until you hit quota walls.

Step 7: Hardening for production

Three changes separate a toy from a tool:

  • Timeout: wrap execute in Promise.race with a 120s guard.
  • Diffing: before Write, have the agent run git diff and return a summary block you parse.
  • Allowlist: never pass Bash with full shell; use a custom tool that only accepts npm test and tsc.

Example timeout:

async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
  return Promise.race([
    p,
    new Promise<T>((_, rej) => setTimeout(() => rej(new Error("timeout")), ms)),
  ]);
}

Call withTimeout(agent.execute(task), 120_000).

What you have

You built a claude agent sdk coding agent that writes code, runs tests, and streams its reasoning. The core is ~40 lines. The SDK’s query function handles context compaction, tool orchestration, and result aggregation—you own the policy, not the plumbing.

Extend it with a retrieval tool for your internal docs, or a PR tool that opens a GitHub branch. The agent loop stays the same.

Tagsclaude-agent-sdkcoding-agentstutorialcustom-agent

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All autonomous coding agents: claude code, devin, cursor posts →