DEV Community

Dinesh_gowtham
Dinesh_gowtham

Posted on

How to Run Claude‑Powered AI Code Review in CodeBuild: A Step‑By‑Step Guide

CI pipelines catch syntax errors, but they still let subtle logic bugs slip through. Pair Claude with CodeBuild and let an AI read every pull request, suggest fixes, and even commit them automatically. The result? Code reviews that never sleep.


Why AI Code Review in CI Matters

When a developer pushes a pull request, the code sits in a queue waiting for a human reviewer. That waiting period can be minutes, hours, or even days. During that time, a simple typo or a misunderstood requirement can slip into production if the review is rushed.

In plain English: Running an AI reviewer inside your continuous‑integration (CI) system gives you a second set of eyes that never takes a coffee break.

The benefit of moving the assistant to CI

  • Consistency – the same prompt and model are used for every build, so the style of feedback doesn’t drift.
  • Zero manual steps – once the pipeline is configured, the AI runs automatically on every commit.
  • Early feedback – bugs are caught before they merge, reducing the cost of fixing them later.

Think of the CI stage as a security guard at the front door. The guard checks every visitor (code change) before they enter the building (main branch). Adding Claude is like giving the guard a pair of X‑ray glasses: it can see hidden issues that a regular guard would miss.


Setting Up a CodeBuild Project for Claude Calls

Before we can ask Claude to review code, we need a CodeBuild project that can talk to the Claude API. The tricky part is keeping the API key safe.

1️⃣ Store the Claude API key in Secrets Manager

  • Secrets Manager – an AWS service that stores sensitive strings (like passwords) and returns them on demand.
  • Why not put the key directly in the buildspec? – CodeBuild truncates environment variables larger than 4 KB, so a raw key longer than that gets cut off, breaking the request.

Create a secret (e.g., claude/api-key) in the console or via CLI:

aws secretsmanager create-secret \
  --name claude/api-key \
  --secret-string "$(cat ~/.claude_key)"
Enter fullscreen mode Exit fullscreen mode

2️⃣ Reference the secret in the buildspec

The ${secretsmanager:ARN} syntax tells CodeBuild to fetch the secret at runtime and inject it as an environment variable. The value is masked in logs, preventing accidental exposure.

# buildspec.yml
version: 0.2

env:
  secrets-manager:
    CLAUDE_API_KEY: "arn:aws:secretsmanager:us-east-1:123456789012:secret:claude/api-key-abc123"

phases:
  install:
    runtime-versions:
      nodejs: 22
    commands:
      - echo "Installing dependencies..."
      - npm ci
  build:
    commands:
      - echo "Running AI code review..."
      - node review.js
artifacts:
  files:
    - diff-output.json
Enter fullscreen mode Exit fullscreen mode

Tip: CodeBuild logs hide any variable that comes from Secrets Manager, but you still want to avoid printing the variable yourself.

3️⃣ Create the CodeBuild project with the AWS SDK

Below is a tiny script that creates (or updates) the project. It uses the @aws-sdk/client-codebuild package, which is the official JavaScript client for CodeBuild.

// create-project.ts
import { CodeBuildClient, CreateProjectCommand, UpdateProjectCommand } from "@aws-sdk/client-codebuild";

// Initialise the client – it reads credentials from the usual places (env, shared config, etc.)
const client = new CodeBuildClient({ region: "us-east-1" });

async function ensureProject() {
  const projectName = "ClaudeCodeReview";

  try {
    // Try to fetch the project; if it exists we will update it.
    const existing = await client.send(new CreateProjectCommand({ /* ... */ }));
    console.log(`Project ${projectName} already exists – skipping creation.`);
  } catch (err) {
    // If the project does not exist we fall back to creating it.
    if (err.name === "ResourceNotFoundException") {
      console.log(`Creating project ${projectName}...`);
      await client.send(new CreateProjectCommand({
        name: projectName,
        source: {
          type: "CODECOMMIT",
          location: "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo"
        },
        environment: {
          type: "LINUX_CONTAINER",
          image: "aws/codebuild/standard:7.0", // Node 22 available here
          computeType: "BUILD_GENERAL1_SMALL",
          environmentVariables: [
            // Reference secret via the special syntax
            { name: "CLAUDE_API_KEY", value: "${secretsmanager:arn:aws:secretsmanager:us-east-1:123456789012:secret:claude/api-key-abc123}", type: "PLAINTEXT" }
          ]
        },
        // Ensure logs do not accidentally print env vars
        logsConfig: { cloudWatchLogs: { status: "ENABLED", groupName: "/aws/codebuild/ClaudeCodeReview" } }
      }));
      console.log(`Project ${projectName} created.`);
    } else {
      // Any other error we surface – this helps debugging mis‑configurations.
      console.error("Unexpected error:", err);
    }
  }
}

ensureProject().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Key takeaway: Using Secrets Manager avoids the 4 KB truncation problem and keeps the key hidden from logs.


Writing the Node.js Agent: Prompt, Call, and Parse

The heart of the solution is a small Node script (review.js) that:

  1. Reads the changed files from the pull request (we’ll assume they are already checked out by CodeBuild).
  2. Sends a prompt to Claude’s /v1/complete endpoint.
  3. Receives a JSON diff suggestion and stores it in S3.

1️⃣ Building the prompt

Claude works best when you give it a clear instruction and a concrete example. The prompt we send is:

You are a senior software engineer. Review the following TypeScript file and suggest a minimal diff that fixes any logic errors, missing edge‑case handling, or performance issues. Return ONLY a JSON object:
{
  "file": "path/to/file.ts",
  "diff": "unified diff string"
}
Enter fullscreen mode Exit fullscreen mode

2️⃣ The agent code

// review.js
import { readFileSync, readdirSync } from "fs";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda";

// Node 22 has a built‑in fetch implementation
const CLAUDE_ENDPOINT = "https://api.anthropic.com/v1/complete";
const MODEL = "claude-3-5-sonnet-20241022"; // example model name

// Helper: exponential backoff for retrying transient errors
async function retry(fn, attempts = 3, delay = 500) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === attempts - 1) throw err; // give up on the last try
      const wait = delay * Math.pow(2, i);
      console.warn(`Retry ${i + 1}/${attempts} after ${wait}ms:`, err.message);
      await new Promise(r => setTimeout(r, wait));
    }
  }
}

// 1️⃣ Collect changed files (simple glob for demo)
function getChangedFiles() {
  // In a real pipeline you would use `git diff --name-only $BASE $HEAD`
  // Here we just read everything under src/
  return readdirSync("./src", { withFileTypes: true })
    .filter(e => e.isFile() && e.name.endsWith(".ts"))
    .map(e => `src/${e.name}`);
}

// 2️⃣ Build the request payload for Claude
function buildPayload(filePath, fileContent) {
  return {
    model: MODEL,
    max_tokens: 1024,
    temperature: 0,
    prompt: `You are a senior software engineer. Review the following TypeScript file and suggest a minimal diff that fixes any logic errors, missing edge‑case handling, or performance issues. Return ONLY a JSON object:\n\nFile: ${filePath}\n\`\`\`typescript\n${fileContent}\n\`\`\``,
  };
}

// 3️⃣ Call Claude with proper headers and error handling
async function callClaude(payload) {
  const response = await retry(() => fetch(CLAUDE_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.CLAUDE_API_KEY, // injected from Secrets Manager
    },
    body: JSON.stringify(payload),
  }));
  if (!response.ok) {
    const errBody = await response.text();
    throw new Error(`Claude returned ${response.status}: ${errBody}`);
  }
  const data = await response.json();
  // Claude returns the text inside a "completion" field
  return JSON.parse(data.completion);
}

// 4️⃣ Store the diff in S3 for later consumption
async function uploadDiff(diffObj) {
  const s3 = new S3Client({ region: "us-east-1" });
  const key = `review-results/${Date.now()}-${diffObj.file}.json`;
  await s3.send(new PutObjectCommand({
    Bucket: "my-ai-review-bucket",
    Key: key,
    Body: JSON.stringify(diffObj, null, 2),
    ContentType: "application/json",
  }));
  console.log(`✅ Diff uploaded to s3://${key}`);
}

// 5️⃣ Rotate the Claude API key via Lambda (security best practice)
async function rotateKey() {
  const lambda = new LambdaClient({ region: "us-east-1" });
  const cmd = new InvokeCommand({
    FunctionName: "RotateClaudeKey",
    InvocationType: "RequestResponse",
  });
  const result = await lambda.send(cmd);
  if (result.FunctionError) {
    throw new Error(`Lambda rotation failed: ${result.FunctionError}`);
  }
  console.log("🔄 Claude API key rotated successfully.");
}

// Main driver
(async () => {
  try {
    const files = getChangedFiles();
    for (const file of files) {
      const content = readFileSync(file, "utf-8");
      const payload = buildPayload(file, content);
      const diff = await callClaude(payload);
      await uploadDiff(diff);
    }

    // Optional: rotate the key after the build to limit exposure time
    await rotateKey();
  } catch (err) {
    // Distinguish network problems from CodeBuild specific errors
    if (err.name === "CodeBuildException") {
      console.error("❗ CodeBuild error:", err);
    } else {
      console.error("❗ Unexpected error:", err);
    }
    process.exit(1); // signal failure to CodeBuild
  }
})();
Enter fullscreen mode Exit fullscreen mode

What each block does

  • retry – tries the fetch again if a transient network glitch occurs, waiting longer each time (exponential backoff).
  • getChangedFiles – a placeholder that pretends every file under src/ is changed; replace with a real git diff in production.
  • buildPayload – creates the JSON body Claude expects, embedding the file content inside a fenced code block.
  • callClaude – performs the HTTP request, checks the HTTP status, parses the completion field, and finally turns the text into a JavaScript object.
  • uploadDiff – writes the AI‑generated diff to an S3 bucket, making it easy for downstream steps (e.g., a bot that opens a PR comment).
  • rotateKey – demonstrates how to invoke a Lambda that rotates the secret; this keeps the key’s lifespan short.

Analogy: Think of retry as a polite knock on a door. If the person inside doesn’t answer the first time, you wait a bit longer and try again, rather than giving up immediately.


Integrating Claude’s Tool Use with Lambda for Secure Secrets

Keeping the Claude API key in the build environment is convenient but risky. A better pattern is to let a short‑lived Lambda fetch the secret, hand it to the build, and then immediately rotate it.

1️⃣ The Lambda function (rotate-key.ts)

// rotate-key.ts
import { SecretsManagerClient, GetSecretValueCommand, PutSecretValueCommand } from "@aws-sdk/client-secrets-manager";

/**
 * This Lambda reads the current Claude key, generates a new one
 * (here we just mock it), stores the new key, and returns the ARN.
 * In a real world you would call the Claude provider’s key‑rotation API.
 */
export const handler = async () => {
  const sm = new SecretsManagerClient({ region: "us-east-1" });
  const secretArn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:claude/api-key-abc123";

  // 1️⃣ Retrieve current secret (optional – we may just replace it)
  const current = await sm.send(new GetSecretValueCommand({ SecretId: secretArn }));
  console.log("Current key fetched (masked).");

  // 2️⃣ Generate a fresh mock key – replace with real rotation logic
  const newKey = "new-mocked-claude-key-" + Date.now();

  // 3️⃣ Store the new key, overwriting the old value
  await sm.send(new PutSecretValueCommand({
    SecretId: secretArn,
    SecretString: newKey,
  }));

  console.log("Secret rotated.");
  return { status: "ok", newKeyArn: secretArn };
};
Enter fullscreen mode Exit fullscreen mode

Deploy the function (Node 22 runtime) and give it permission to read/write the secret.

Tip: The Lambda runs inside a VPC that has a NAT gateway if your secret is encrypted with a KMS key that lives in a private subnet. Without the NAT gateway the Lambda cannot reach the KMS service.

2️⃣ Calling the Lambda from the build agent

In the review.js script we already imported LambdaClient and used InvokeCommand. The key points:

  • InvocationType: "RequestResponse" makes the build wait for the Lambda to finish, so we know the rotation succeeded before the build ends.
  • Error handling – if the Lambda returns FunctionError, we treat it as a failure and abort the build.

In plain English: The build asks Lambda to change the lock on the safe (the secret). If the lock can’t be changed, the build stops so you don’t continue with a compromised key.


Handling Rate Limits and Retry Logic

Claude’s API enforces a per‑minute request quota. If your repository has many changed files, you can quickly hit that limit.

1️⃣ Detecting a rate‑limit response

Claude returns HTTP 429 when the quota is exhausted. Our callClaude function already throws an error for any non‑2xx status, but we can be more specific:

async function callClaude(payload) {
  const response = await retry(() => fetch(CLAUDE_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.CLAUDE_API_KEY,
    },
    body: JSON.stringify(payload),
  }));
  if (response.status === 429) {
    const reset = response.headers.get("retry-after") || "60"; // seconds
    throw new Error(`Rate limit hit – retry after ${reset}s`);
  }
  // …rest unchanged
}
Enter fullscreen mode Exit fullscreen mode

2️⃣ Back‑off strategy

When we catch a 429 we wait the Retry-After header (or a default) before trying again. The retry helper already multiplies the delay, but for rate limits we want a linear wait:

async function callClaudeWithBackoff(payload) {
  let attempt = 0;
  while (attempt < 5) {
    try {
      return await callClaude(payload);
    } catch (err) {
      if (err.message.includes("Rate limit")) {
        const waitSec = parseInt(err.message.split("after ")[1]) || 60;
        console.warn(`⏳ Waiting ${waitSec}s before retry #${attempt + 1}`);
        await new Promise(r => setTimeout(r, waitSec * 1000));
      } else {
        // Re‑throw other errors to be handled by the outer catch
        throw err;
      }
    }
    attempt++;
  }
  throw new Error("Exceeded max retries due to rate limits");
}
Enter fullscreen mode Exit fullscreen mode

Replace the call in the main loop with callClaudeWithBackoff.

Key takeaway: Explicitly handling 429 lets the pipeline pause instead of failing outright, which is especially helpful for large PRs.


The Takeaway

What you now have: a CI‑integrated AI reviewer that runs automatically, stores its suggestions safely, and keeps its credentials rotating.

  • Store secrets in Secrets Manager and reference them with ${secretsmanager:…} to avoid the 4 KB truncation bug.
  • Mask environment variables in CodeBuild logs; otherwise, plain‑text values become visible.
  • Write a small Node agent that builds a clear prompt, calls Claude with native fetch, parses the JSON diff, and writes it to S3.
  • Rotate the Claude API key via Lambda so the key lives only for the duration of a single build.
  • Detect and respect Claude’s 429 rate‑limit by reading the Retry-After header and applying a linear back‑off.
  • Remember the CI‑layer advantages: consistency, zero manual steps, and early feedback that catches bugs before they merge.

Now you can add AI‑powered code reviews to any repository that already uses CodeBuild—no extra IDE plugins, no nightly jobs, just a smarter pipeline that watches over your code 24/7. Happy building!


Transparency notice

This article was written with the help of an AI system — Groq (GPT OSS 120B).

Published: 2026-10-21 · Primary focus: CodeBuild

All code blocks are intended to be correct and runnable, but please verify them
against the official docs for the tools mentioned before using in production.

Find an error? Drop a comment — corrections are always welcome.

Top comments (0)