Build an AI Code Review Agent

Build a CLI agent that uses Claude tool use to review code files.

Introduction

30 Second Summary

Every developer has reviewed code before. You scan for bugs, check for security issues, and suggest improvements. Now imagine giving an AI agent access to your filesystem so it can do the same thing automatically.

In this project, you will build a command-line code review agent powered by Claude. Your agent reads files from your machine, analyzes them for issues, and returns structured feedback with severity levels and fix suggestions.

What You'll Build

A TypeScript CLI application that uses the Anthropic API with tool use to review any code file on your machine and output categorized issues as structured JSON.

By the end of this project, you'll have:

  • A working CLI tool that sends code to Claude and receives structured review feedback with issues, severity levels, and suggestions.
  • Hands-on experience building the agent loop pattern that powers Claude Code and similar AI products.
  • Custom tool definitions that give Claude the ability to read files and explore directories on your filesystem.
  • Secret Mission: extend your agent with a write tool so Claude can auto-fix the code it reviews.

Are there any prerequisites?

You should have basic familiarity with TypeScript or JavaScript and be comfortable using npm. You will also need Node.js 18 or higher installed on your machine.

Before We Start

Before We Start...

Set Up Your Project

To build your AI code review agent, you need a TypeScript project with the Anthropic SDK installed and a way to run TypeScript files directly.

In this step, you'll scaffold the project, connect to the Claude API, and create a sample file with intentional issues for your agent to review later.

In this step, get ready to:
  • Create the code-review-agent project directory and install dependencies.
  • Set your Anthropic API key and verify the connection.
  • Create a sample file with intentional code issues.
Create your project directory and install dependencies

Your agent needs a dedicated folder with Node.js package management configured for ES modules and two packages: the Anthropic SDK for talking to Claude, and tsx for running TypeScript files without a compile step.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir code-review-agent
cd code-review-agent
  • Confirm the folder was created by running:
ls

You should see an empty listing since you're now inside the new folder.

  • Initialize your project and configure it for ES modules by running these commands:
npm init -y
npm pkg set type=module

What do these commands do?

  • npm init -y creates a package.json file with default settings. This file tracks your project's dependencies.
  • npm pkg set type=module configures your project to use ES module syntax (import/export) instead of the older CommonJS format.
  • Install the Anthropic SDK by running:
npm install @anthropic-ai/sdk
  • Install tsx as a development dependency by running:
npm install -D tsx

What is tsx?

tsx lets you run TypeScript files directly without a separate compile step. It uses esbuild under the hood for fast execution.

  • Verify tsx is installed correctly by running:
npx tsx --version

You should see a version number printed in your terminal.

Not seeing a version number?

  • Make sure you ran npm install -D tsx from inside the code-review-agent folder. If you're in the wrong directory, the package won't be found.
  • Check that your Node.js version is 18+ by running node --version. Older versions may not support the required module features.

Help me troubleshoot my tsx installation

Set your Anthropic API key and verify the connection

The Anthropic SDK reads your API key from an environment variable called ANTHROPIC_API_KEY. You already have your key from the Anthropic console. Now you need to make it available in your terminal session.

macOS

  • Set the environment variable by running this command, replacing the placeholder with your actual key:
export ANTHROPIC_API_KEY=your-api-key-here

Windows

Choose the command that matches your terminal.

  • If you are using Command Prompt, run:
set ANTHROPIC_API_KEY=your-api-key-here
  • If you are using PowerShell, run:
$env:ANTHROPIC_API_KEY="your-api-key-here"

Why an environment variable?

Storing your key in an environment variable keeps it out of your source code. The Anthropic SDK automatically reads ANTHROPIC_API_KEY without you needing to pass it in your code.

This variable only lasts for your current terminal session. If you close and reopen your terminal, you'll need to set it again.

Now let's verify the connection works by creating a quick test file.

  • Open your code-review-agent folder in your code editor.
  • Create a new file called test.ts and paste the following code:
import Anthropic from "@anthropic-ai/sdk";

// Quick test to verify your API key works
const client = new Anthropic();

const message = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Say hello in one sentence." }],
});

console.log(message.content);

What does this code do?

  • import Anthropic from "@anthropic-ai/sdk" loads the SDK.
  • new Anthropic() creates a client that automatically uses your ANTHROPIC_API_KEY environment variable.
  • client.messages.create(...) sends a message to Claude and returns the response.
  • Save test.ts.
  • Run the test file by switching to your terminal and running:
npx tsx test.ts

You should see a response from Claude printed in your terminal, confirming that your API key and SDK are working correctly.

Seeing an authentication error?

  • Double-check that you set the environment variable in the same terminal session where you ran the command. Opening a new tab or window resets it.
  • Make sure your API key is correct. You can verify it at console.anthropic.com/settings/keys.
  • Ensure there are no extra spaces or quotes around the key value.

Help me debug my Anthropic API authentication error

Create a sample file to review

Your code review agent needs something to review. You'll create a TypeScript file with several intentional issues: unused variables, missing error handling, poor naming, and a hardcoded secret.

  • Create a new file called sample.ts in your code-review-agent folder and paste the following code:
// Sample code with intentional issues for the reviewer to find

const x = 42;
let unusedVar = "hello";

function fetchdata(url) {
  const response = fetch(url);
  return response;
}

function processItems(items: any[]) {
  for (let i = 0; i < items.length; i++) {
    console.log(items[i]);
  }
  return items;
}

const API_KEY = "sk-1234567890abcdef";

export { fetchdata, processItems };

What issues are hiding in this code?

  • x and unusedVar are declared but never used anywhere.
  • fetchdata uses inconsistent naming (should be camelCase) and is missing await on the fetch() call.
  • processItems uses any[] which defeats TypeScript's type safety.
  • API_KEY is a hardcoded secret, which is a security vulnerability.
  • Save sample.ts.
  • Confirm the file exists by running:
ls

You should see sample.ts, test.ts, package.json, and a node_modules folder listed.

✔️ Awesome, I've got everything!

Great. Double-check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete sample.ts file:

// Sample code with intentional issues for the reviewer to find

const x = 42;
let unusedVar = "hello";

function fetchdata(url) {
  const response = fetch(url);
  return response;
}

function processItems(items: any[]) {
  for (let i = 0; i < items.length; i++) {
    console.log(items[i]);
  }
  return items;
}

const API_KEY = "sk-1234567890abcdef";

export { fetchdata, processItems };

Your project environment is ready and confirmed working. Next up, you'll define the tools that let Claude read files on your machine.

Define Your Tools

Your project is set up and Claude can respond to basic prompts. Now it's time to give Claude the ability to interact with your filesystem.

Claude's tool use feature lets you define capabilities that Claude can request during a conversation. Claude never executes tools directly. Instead, your code defines what tools exist, and your code runs them when Claude asks.

In this step, get ready to:
  • Define tool schemas that tell Claude what filesystem operations are available.
  • Build handler functions that execute each tool and return results.
  • Wire up a dispatcher that routes tool calls to the correct handler.
Create the tool definitions

Every tool you give Claude needs a definition that describes what the tool does and what inputs it expects. These definitions follow the JSON Schema format so Claude can understand the parameters.

  • Create a new file called review.ts in your code-review-agent folder.
  • Add the following code to set up the Anthropic client and define two tools:
import Anthropic from "@anthropic-ai/sdk";
import { readFileSync, readdirSync } from "fs";
import { resolve } from "path";

const client = new Anthropic();

// Tool definitions
const tools: Anthropic.Tool[] = [
  {
    name: "read_file",
    description:
      "Read the contents of a file at the given path. Use this to read source code files for review.",
    input_schema: {
      type: "object" as const,
      properties: {
        path: {
          type: "string",
          description: "The relative or absolute path to the file to read",
        },
      },
      required: ["path"],
    },
  },
  {
    name: "list_directory",
    description:
      "List the files and directories at the given path. Use this to explore the project structure.",
    input_schema: {
      type: "object" as const,
      properties: {
        path: {
          type: "string",
          description: "The relative or absolute path to the directory to list",
        },
      },
      required: ["path"],
    },
  },
];

What does this code do?

The client reads the ANTHROPIC_API_KEY environment variable you set in Step 1 automatically. No need to pass it explicitly.

Each tool definition has three fields: name (what Claude uses to request it), description (helps Claude decide when to use it), and input_schema (what parameters the tool accepts). Claude reads these descriptions to decide which tool to call and what arguments to pass.

Why two tools instead of one?

The read_file tool lets Claude read specific files, while list_directory lets it explore what files exist. Together, they give Claude the same filesystem awareness you have when reviewing code manually.

  • Save review.ts.
Build the tool execution function

Tool definitions only tell Claude what's available. You still need the actual code that runs when Claude requests a tool. This dispatch function receives a tool name from Claude and routes it to the correct handler.

  • In review.ts, add the following code below the tools array:
// Tool execution
function processToolCall(
  toolName: string,
  toolInput: Record<string, string>
): string {
  switch (toolName) {
    case "read_file": {
      try {
        const filePath = resolve(toolInput.path);
        const content = readFileSync(filePath, "utf-8");
        return content;
      } catch (error) {
        return `Error reading file: ${(error as Error).message}`;
      }
    }
    case "list_directory": {
      try {
        const dirPath = resolve(toolInput.path);
        const entries = readdirSync(dirPath, { withFileTypes: true });
        return entries
          .map((e) => `${e.isDirectory() ? "[dir]" : "[file]"} ${e.name}`)
          .join("\n");
      } catch (error) {
        return `Error listing directory: ${(error as Error).message}`;
      }
    }
    default:
      return `Unknown tool: ${toolName}`;
  }
}

What does this code do?

The processToolCall function is the bridge between Claude's requests and your filesystem. When Claude says "I want to use read_file with path sample.ts", this function receives that request and actually reads the file.

The try/catch blocks are important. If a file doesn't exist, the function returns a descriptive error message instead of crashing. Claude can then read that error and adjust its approach.

  • Save review.ts.
  • Verify TypeScript compiles cleanly by running:
npx tsx review.ts

The script should exit silently with no errors. There's no output yet because you haven't added an entry point that calls the functions.

Seeing a syntax or import error?

  • Check that your package.json has "type": "module" set. Without it, the import syntax won't work.
  • Make sure the import from @anthropic-ai/sdk uses a default import (import Anthropic), not a named import (import { Anthropic }).
  • Confirm you're running the command from inside the code-review-agent directory where node_modules lives.

Why won't my review.ts file compile?

✔️ Awesome, I've got everything!

Great. Make sure you've saved review.ts before moving on.

ⓧ I'd like to double check the full code

Here's the complete review.ts so far:

import Anthropic from "@anthropic-ai/sdk";
import { readFileSync, readdirSync } from "fs";
import { resolve } from "path";

const client = new Anthropic();

// Tool definitions
const tools: Anthropic.Tool[] = [
  {
    name: "read_file",
    description:
      "Read the contents of a file at the given path. Use this to read source code files for review.",
    input_schema: {
      type: "object" as const,
      properties: {
        path: {
          type: "string",
          description: "The relative or absolute path to the file to read",
        },
      },
      required: ["path"],
    },
  },
  {
    name: "list_directory",
    description:
      "List the files and directories at the given path. Use this to explore the project structure.",
    input_schema: {
      type: "object" as const,
      properties: {
        path: {
          type: "string",
          description: "The relative or absolute path to the directory to list",
        },
      },
      required: ["path"],
    },
  },
];

// Tool execution
function processToolCall(
  toolName: string,
  toolInput: Record<string, string>
): string {
  switch (toolName) {
    case "read_file": {
      try {
        const filePath = resolve(toolInput.path);
        const content = readFileSync(filePath, "utf-8");
        return content;
      } catch (error) {
        return `Error reading file: ${(error as Error).message}`;
      }
    }
    case "list_directory": {
      try {
        const dirPath = resolve(toolInput.path);
        const entries = readdirSync(dirPath, { withFileTypes: true });
        return entries
          .map((e) => `${e.isDirectory() ? "[dir]" : "[file]"} ${e.name}`)
          .join("\n");
      } catch (error) {
        return `Error listing directory: ${(error as Error).message}`;
      }
    }
    default:
      return `Unknown tool: ${toolName}`;
  }
}

Your tools are defined and your execution layer is ready. Next up, you'll build the agent loop that connects Claude to these tools in a back-and-forth conversation.

Build the Agent Loop

You have tools defined and handlers ready to execute them. The missing piece is the orchestration logic that ties everything together.

In this step, you will build the agent loop. This is the core pattern that powers AI products like Claude Code. Your code sends a message to Claude, detects when Claude wants to use a tool, executes that tool, returns the result, and repeats until Claude has enough information to produce a final answer.

In this step, get ready to:
  • Add a system prompt that tells Claude how to behave as a code reviewer.
  • Implement the agent loop that handles tool calls and loops until Claude is done.
  • Test the loop by running a review on your sample file.
Add the system prompt

The system prompt tells Claude what role to play and what to do when it receives a review request. This is how you control the agent's behavior without hardcoding logic into your application.

  • In review.ts, find the processToolCall function's closing brace. Add the following system prompt below it:
// System prompt
const systemPrompt = `You are an expert code reviewer. When asked to review a file:
1. Use the read_file tool to read the file contents.
2. Analyze the code for issues.`;

What does this code do?

The system prompt is a set of instructions that Claude follows for every message in the conversation. It tells Claude to use the read_file tool to read code, then analyze it for issues.

You will expand this prompt in the next step to request structured JSON output. For now, a simple instruction is enough to verify the loop works.

  • Save review.ts.
Implement the agent loop function

The agent loop is a while loop that calls the Claude API, checks the response, and decides what to do next. If Claude wants to use a tool, the loop executes it and sends the result back. If Claude is done, the loop exits and returns the final text.

  • Below the systemPrompt you just added, add the full runReviewAgent function by pasting this code:
// Agent loop
async function runReviewAgent(filePath: string): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    {
      role: "user",
      content: `Please review the file at path: ${filePath}`,
    },
  ];

  let iterations = 0;
  const maxIterations = 10;

  while (iterations < maxIterations) {
    iterations++;

    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 4096,
      system: systemPrompt,
      tools,
      messages,
    });

    // If Claude is done (no more tool use), return the final text
    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find((block) => block.type === "text");
      return textBlock ? textBlock.text : "No review generated.";
    }

    // If Claude wants to use tools, execute them
    if (response.stop_reason === "tool_use") {
      // Add assistant's response to conversation
      messages.push({ role: "assistant", content: response.content });

      // Process each tool use block
      const toolResults: Anthropic.ToolResultBlockParam[] = [];
      for (const block of response.content) {
        if (block.type === "tool_use") {
          const result = processToolCall(
            block.name,
            block.input as Record<string, string>
          );
          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: result,
          });
        }
      }

      // Add tool results to conversation
      messages.push({ role: "user", content: toolResults });
    }
  }

  return "Max iterations reached without completing review.";
}

What does this function do?

This is the full agent loop. Here is how each section works:

  • messages starts with a single user message asking Claude to review the file. This array grows as the conversation progresses.
  • client.messages.create() sends the full conversation to Claude with the tools array available. Claude decides whether to respond with text or request a tool call.
  • When stop_reason is "end_turn", Claude is finished. The function extracts the text block and returns it.
  • When stop_reason is "tool_use", Claude wants to call a tool. The loop pushes the assistant's response into the conversation, executes each requested tool, and appends the results as a user message.
  • maxIterations prevents infinite loops. If Claude keeps requesting tools beyond 10 rounds, the function exits safely.

Why does tool_use require a specific message structure?

Claude's API enforces a strict conversation format. When Claude responds with a tool_use block, the next message in the conversation MUST be a user message containing tool_result blocks. Each result references the original tool call by its tool_use_id.

This is how Claude knows which tool produced which result. It is the same pattern used in production AI agents.

  • Save review.ts.

Seeing red underlines in your editor?

If your editor shows type errors on Anthropic.MessageParam or Anthropic.ToolResultBlockParam, make sure the import at the top of your file is import Anthropic from "@anthropic-ai/sdk". These types are exported from the SDK's default export.

help me fix TypeScript errors in my agent loop function

Test the agent loop

The function is ready but nothing calls it yet. You will add a proper CLI entry point in the next step. For now, add a temporary call at the bottom of the file to verify the loop works.

  • At the very bottom of review.ts, add this temporary test call:
// Temporary test call - you'll replace this in the next step
runReviewAgent("sample.ts").then((result) => console.log(result));
  • Save review.ts.
  • Run the agent by executing this command:
npx tsx review.ts

You should see Claude's review of your sample.ts file printed to the terminal. It will mention issues like unused variables, missing error handling, and the hardcoded API key.

What just happened behind the scenes?

Your agent loop made at least two API calls. The first call told Claude to review the file. Claude responded with a tool_use request for read_file. Your loop executed that tool, sent the file contents back, and Claude made its second call returning the review text.

Getting an authentication error?

Make sure your ANTHROPIC_API_KEY environment variable is still set in your current terminal session. If you opened a new terminal, you need to set it again.

help me debug my Anthropic API authentication error

  • Once you see the review output, remove the temporary test call from the bottom of review.ts (the two lines starting with // Temporary test call).
  • Save review.ts.

✔️ Awesome, I've got everything!

Great. Double check you have saved review.ts without the temporary test call at the bottom.

ⓧ I'd like to double check the full code

Here is the complete review.ts at this point (without the temporary test call):

import Anthropic from "@anthropic-ai/sdk";
import { readFileSync, readdirSync } from "fs";
import { resolve } from "path";

const client = new Anthropic();

const tools: Anthropic.Tool[] = [
  {
    name: "read_file",
    description:
      "Read the contents of a file at the given path. Use this to read source code files for review.",
    input_schema: {
      type: "object" as const,
      properties: {
        path: {
          type: "string",
          description: "The relative or absolute path to the file to read",
        },
      },
      required: ["path"],
    },
  },
  {
    name: "list_directory",
    description:
      "List the files and directories at the given path. Use this to explore the project structure.",
    input_schema: {
      type: "object" as const,
      properties: {
        path: {
          type: "string",
          description: "The relative or absolute path to the directory to list",
        },
      },
      required: ["path"],
    },
  },
];

function processToolCall(
  toolName: string,
  toolInput: Record<string, string>
): string {
  switch (toolName) {
    case "read_file": {
      try {
        const filePath = resolve(toolInput.path);
        const content = readFileSync(filePath, "utf-8");
        return content;
      } catch (error) {
        return `Error reading file: ${(error as Error).message}`;
      }
    }
    case "list_directory": {
      try {
        const dirPath = resolve(toolInput.path);
        const entries = readdirSync(dirPath, { withFileTypes: true });
        return entries
          .map((e) => `${e.isDirectory() ? "[dir]" : "[file]"} ${e.name}`)
          .join("\n");
      } catch (error) {
        return `Error listing directory: ${(error as Error).message}`;
      }
    }
    default:
      return `Unknown tool: ${toolName}`;
  }
}

const systemPrompt = `You are an expert code reviewer. When asked to review a file:
1. Use the read_file tool to read the file contents.
2. Analyze the code for issues.`;

async function runReviewAgent(filePath: string): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    {
      role: "user",
      content: `Please review the file at path: ${filePath}`,
    },
  ];

  let iterations = 0;
  const maxIterations = 10;

  while (iterations < maxIterations) {
    iterations++;

    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 4096,
      system: systemPrompt,
      tools,
      messages,
    });

    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find((block) => block.type === "text");
      return textBlock ? textBlock.text : "No review generated.";
    }

    if (response.stop_reason === "tool_use") {
      messages.push({ role: "assistant", content: response.content });

      const toolResults: Anthropic.ToolResultBlockParam[] = [];
      for (const block of response.content) {
        if (block.type === "tool_use") {
          const result = processToolCall(
            block.name,
            block.input as Record<string, string>
          );
          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: result,
          });
        }
      }

      messages.push({ role: "user", content: toolResults });
    }
  }

  return "Max iterations reached without completing review.";
}

Your agent loop is working. Claude reads your file through the read_file tool and returns a review. Next up, you will structure that review into clean JSON output and wire up a proper CLI entry point.

Structure the Output

Your agent loop works. Claude calls your tools, reads files, and produces a review. But the response comes back as free-form text, which makes it impossible to parse programmatically or display in a consistent format.

In this step, you'll use system prompt engineering to instruct Claude to return structured JSON with categorized issues. Then you'll build a CLI entry point that formats the output with color-coded severity levels.

In this step, get ready to:
  • Update the system prompt to request structured JSON output.
  • Build a CLI entry point that parses and displays color-coded results.
  • Run the agent against your sample file to see the full review.
Update the system prompt for structured JSON output

The system prompt controls how Claude formats its response. By specifying an exact JSON schema in the prompt, you can reliably parse the output every time.

  • In review.ts, find the existing systemPrompt variable. It currently looks like this:
const systemPrompt = `You are an expert code reviewer. When asked to review a file:
1. Use the read_file tool to read the file contents.
2. Analyze the code for issues.`;
  • Replace the entire systemPrompt declaration with this expanded version:
// System prompt
const systemPrompt = `You are an expert code reviewer. When asked to review a file:
1. Use the read_file tool to read the file contents.
2. Analyze the code for issues including: bugs, security vulnerabilities, performance problems, style issues, and missing error handling.
3. Return your review as a JSON object with this exact structure:

{
  "issues": [
    {
      "line": <number>,
      "severity": "error" | "warning" | "info",
      "category": "<string>",
      "message": "<string>",
      "suggestion": "<string>"
    }
  ],
  "summary": {
    "total": <number>,
    "errors": <number>,
    "warnings": <number>,
    "info": <number>
  }
}

Return ONLY the JSON object, no additional text or markdown formatting.`;

What does this prompt do?

The prompt tells Claude exactly what to analyze (bugs, security issues, performance, style, missing error handling) and exactly what shape the output must take.

Each issue in the array includes a line number, a severity level, a category, a human-readable message, and a suggestion for how to fix it. The summary object gives you counts at a glance.

  • While you're at the top of the file, also update the fs import line. Find this line:
import { readFileSync, readdirSync } from "fs";
  • Add writeFileSync to the import so it looks like this:
import { readFileSync, readdirSync, writeFileSync } from "fs";

Why add writeFileSync now?

You'll use writeFileSync in the Secret Mission to write a corrected version of the reviewed file. Adding it now keeps your imports tidy for later.

Add the CLI entry point and display logic

The agent loop function is ready, but there's no way to actually run it from the command line yet. You need a main function that reads the file path argument, calls the agent, parses the JSON response, and prints formatted results.

  • Add the following main function at the bottom of review.ts, after the runReviewAgent function:
// CLI entry point
async function main() {
  const filePath = process.argv[2];

  if (!filePath) {
    console.error("Usage: npx tsx review.ts <file-path>");
    process.exit(1);
  }

  console.log(`Reviewing: ${filePath}\n`);

  try {
    const result = await runReviewAgent(filePath);
    const review = JSON.parse(result);

    // Display formatted output
    console.log("=== Code Review Results ===\n");
    console.log(
      `Summary: ${review.summary.total} issues found`
    );
    console.log(
      `  Errors: ${review.summary.errors} | Warnings: ${review.summary.warnings} | Info: ${review.summary.info}\n`
    );

    for (const issue of review.issues) {
      const prefix =
        issue.severity === "error"
          ? "\x1b[31m[ERROR]\x1b[0m"
          : issue.severity === "warning"
            ? "\x1b[33m[WARN]\x1b[0m"
            : "\x1b[36m[INFO]\x1b[0m";

      console.log(`${prefix} Line ${issue.line}: ${issue.message}`);
      console.log(`       Category: ${issue.category}`);
      console.log(`       Suggestion: ${issue.suggestion}\n`);
    }
  } catch (error) {
    // If JSON parsing fails, print raw output
    console.error("Failed to parse structured review. Raw output:");
    console.error((error as Error).message);
  }
}

main();

What does this code do?

The function reads the file path from process.argv[2] (the first argument after the script name). If no argument is provided, it prints a usage message and exits.

It calls runReviewAgent and parses the returned string as JSON. The display loop uses ANSI escape codes to color-code each severity level: red for errors, yellow for warnings, and cyan for info.

The catch block handles cases where Claude doesn't return valid JSON. Instead of crashing, it prints the raw output so you can debug.

  • Save review.ts.

Seeing red squiggles in your editor?

Make sure the main() call is the very last line in the file, outside and after the function definition.

Check that your template literal backticks in the systemPrompt are properly closed. A missing backtick will cause syntax errors throughout the rest of the file.

help me fix syntax errors in review.ts

Run the review agent

Everything is wired up. Time to run your agent against the sample file and see the full structured review.

  • Run the review agent against your sample file by running this command:
npx tsx review.ts sample.ts

You should see color-coded output listing issues found in sample.ts. The summary line shows total counts, followed by individual issues with line numbers, categories, and suggestions.

What should I see?

Your output will start with a summary line like Summary: 5 issues found (the exact count depends on what Claude finds). Below that, each issue appears with a colored severity tag, a line number, and a suggestion.

The agent takes a few seconds to respond because it first calls your read_file tool, receives the file contents, then analyzes them and returns the structured JSON.

Seeing "Failed to parse structured review"?

This means Claude returned text that isn't valid JSON. Run the command again. LLM outputs can vary between runs, and occasionally Claude wraps the JSON in markdown code fences despite the prompt instruction.

If it happens repeatedly, double-check that your systemPrompt includes the line Return ONLY the JSON object, no additional text or markdown formatting. at the end.

help me debug why my agent isn't returning valid JSON

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete review.ts file at this point:

import Anthropic from "@anthropic-ai/sdk";
import { readFileSync, readdirSync, writeFileSync } from "fs";
import { resolve } from "path";

const client = new Anthropic();

// Tool definitions
const tools: Anthropic.Tool[] = [
  {
    name: "read_file",
    description:
      "Read the contents of a file at the given path. Use this to read source code files for review.",
    input_schema: {
      type: "object" as const,
      properties: {
        path: {
          type: "string",
          description: "The relative or absolute path to the file to read",
        },
      },
      required: ["path"],
    },
  },
  {
    name: "list_directory",
    description:
      "List the files and directories at the given path. Use this to explore the project structure.",
    input_schema: {
      type: "object" as const,
      properties: {
        path: {
          type: "string",
          description: "The relative or absolute path to the directory to list",
        },
      },
      required: ["path"],
    },
  },
];

// Tool execution
function processToolCall(
  toolName: string,
  toolInput: Record<string, string>
): string {
  switch (toolName) {
    case "read_file": {
      try {
        const filePath = resolve(toolInput.path);
        const content = readFileSync(filePath, "utf-8");
        return content;
      } catch (error) {
        return `Error reading file: ${(error as Error).message}`;
      }
    }
    case "list_directory": {
      try {
        const dirPath = resolve(toolInput.path);
        const entries = readdirSync(dirPath, { withFileTypes: true });
        return entries
          .map((e) => `${e.isDirectory() ? "[dir]" : "[file]"} ${e.name}`)
          .join("\n");
      } catch (error) {
        return `Error listing directory: ${(error as Error).message}`;
      }
    }
    default:
      return `Unknown tool: ${toolName}`;
  }
}

// System prompt
const systemPrompt = `You are an expert code reviewer. When asked to review a file:
1. Use the read_file tool to read the file contents.
2. Analyze the code for issues including: bugs, security vulnerabilities, performance problems, style issues, and missing error handling.
3. Return your review as a JSON object with this exact structure:

{
  "issues": [
    {
      "line": <number>,
      "severity": "error" | "warning" | "info",
      "category": "<string>",
      "message": "<string>",
      "suggestion": "<string>"
    }
  ],
  "summary": {
    "total": <number>,
    "errors": <number>,
    "warnings": <number>,
    "info": <number>
  }
}

Return ONLY the JSON object, no additional text or markdown formatting.`;

// Agent loop
async function runReviewAgent(filePath: string): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    {
      role: "user",
      content: `Please review the file at path: ${filePath}`,
    },
  ];

  let iterations = 0;
  const maxIterations = 10;

  while (iterations < maxIterations) {
    iterations++;

    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 4096,
      system: systemPrompt,
      tools,
      messages,
    });

    // If Claude is done (no more tool use), return the final text
    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find((block) => block.type === "text");
      return textBlock ? textBlock.text : "No review generated.";
    }

    // If Claude wants to use tools, execute them
    if (response.stop_reason === "tool_use") {
      // Add assistant's response to conversation
      messages.push({ role: "assistant", content: response.content });

      // Process each tool use block
      const toolResults: Anthropic.ToolResultBlockParam[] = [];
      for (const block of response.content) {
        if (block.type === "tool_use") {
          const result = processToolCall(
            block.name,
            block.input as Record<string, string>
          );
          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: result,
          });
        }
      }

      // Add tool results to conversation
      messages.push({ role: "user", content: toolResults });
    }
  }

  return "Max iterations reached without completing review.";
}

// CLI entry point
async function main() {
  const filePath = process.argv[2];

  if (!filePath) {
    console.error("Usage: npx tsx review.ts <file-path>");
    process.exit(1);
  }

  console.log(`Reviewing: ${filePath}\n`);

  try {
    const result = await runReviewAgent(filePath);
    const review = JSON.parse(result);

    // Display formatted output
    console.log("=== Code Review Results ===\n");
    console.log(
      `Summary: ${review.summary.total} issues found`
    );
    console.log(
      `  Errors: ${review.summary.errors} | Warnings: ${review.summary.warnings} | Info: ${review.summary.info}\n`
    );

    for (const issue of review.issues) {
      const prefix =
        issue.severity === "error"
          ? "\x1b[31m[ERROR]\x1b[0m"
          : issue.severity === "warning"
            ? "\x1b[33m[WARN]\x1b[0m"
            : "\x1b[36m[INFO]\x1b[0m";

      console.log(`${prefix} Line ${issue.line}: ${issue.message}`);
      console.log(`       Category: ${issue.category}`);
      console.log(`       Suggestion: ${issue.suggestion}\n`);
    }
  } catch (error) {
    // If JSON parsing fails, print raw output
    console.error("Failed to parse structured review. Raw output:");
    console.error((error as Error).message);
  }
}

main();

You now have a fully working CLI code review agent. It reads any file on your machine, sends it to Claude through the tool-use loop, and returns a structured, color-coded review. Next up, the Secret Mission extends your agent with the ability to auto-fix the code it reviews.

Secret mission

Add a Write Tool

Your agent can read code and review it. But what if it could also fix the problems it finds? In this secret mission, you'll add a write_file tool so Claude can generate a corrected version of any file it reviews.

Clean Up Your Resources

Clean Up Your Resources

This project runs locally with no ongoing costs. You only pay for Anthropic API calls when you actively run the tool.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • code-review-agent project folder (contains review.ts, sample.ts, sample.fixed.ts, and node_modules).
  • Anthropic API key (stored in your console.anthropic.com account).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your code review agent or extending it with new tools.

  • Your project files stay on your Desktop ready for next time.
  • Your Anthropic API key remains active. You will only be charged when you run the tool again.
  • The ANTHROPIC_API_KEY environment variable only lasts for your current terminal session. You will need to set it again next time you open a new terminal.

Pause - I'll come back to this later

Close your terminal to clear the environment variable. Your project files remain on disk for when you return.

  • Close your terminal window. This removes the ANTHROPIC_API_KEY environment variable from memory.
  • Your project folder and API key remain intact. No ongoing costs are incurred while the tool is not running.

Delete - I don't want to use this again

Remove all project files and revoke your API key so nothing is left behind.

  • Delete the project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/code-review-agent

Windows

Remove-Item -Recurse -Force ~\Desktop\code-review-agent

Nice Work!

Nice Work!

You just built a fully functional AI code review agent from scratch. Your CLI tool reads files from your filesystem, sends them to Claude with custom tool definitions, and returns structured feedback with severity levels, categories, and actionable suggestions.

You've learned how to:

  • Built a CLI application that uses the Anthropic SDK to send requests to Claude with custom tool definitions.
  • Implemented the full agent loop pattern: defining tools, detecting tool_use stop reasons, executing tool calls, returning results, and looping until Claude produces a final response.
  • Created structured JSON output from an LLM using system prompt engineering, with formatted console display using ANSI color codes.
  • Secret Mission: Extended the agent with a write_file tool so Claude can auto-generate a fixed version of the reviewed code alongside its review.

Ready to quiz yourself?