Build an AI-Powered CLI Security Scanner

Build a CLI tool that uses Claude AI to scan JS/TS files for security vulnerabilities.

Introduction

30 Second Summary

Every developer writes code that could have security holes. Catching vulnerabilities early usually means expensive tools or tedious manual reviews.

In this project, you will build a command-line tool that uses Claude to scan JavaScript and TypeScript files for security vulnerabilities and print a structured report with severity ratings and fix recommendations.

What You'll Build

Point the scanner at any file, run one command, and within seconds you get a numbered security report in your terminal listing every vulnerability Claude found, its severity, and exactly how to fix it.

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

  • A CLI scanner you can point at any JavaScript or TypeScript file containing known vulnerabilities (SQL injection, command injection, XSS, hardcoded secrets) and see Claude identify each one.
  • A robust JSON extraction function that handles messy LLM output (markdown code fences, extra text) without breaking.
  • A formatted terminal report with severity ratings (CRITICAL, HIGH, MEDIUM, LOW), line numbers, and actionable fix recommendations for each finding.
  • Secret Mission: Add color-coded severity output with Chalk, sort findings from most to least severe, and display a severity summary.

Are there any prerequisites?

You should be comfortable with basic Node.js concepts like async/await, npm, and file I/O.

You will also need an Anthropic API key ready to go. If you don't have one yet, you can create a free account at console.anthropic.com and generate a key before starting.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters for your workflow as a developer.

Set Up the Project

Before you can write any scanner logic, you need a configured Node.js project. The scanner depends on ES module imports and the Anthropic SDK, so getting the foundation right here prevents friction in every step that follows.

In this step you will create a project directory, configure it for ES modules, install the Anthropic SDK, and set up the test file your scanner will eventually analyze.

In this step, get ready to:
  • Verify Node.js and initialize a project directory.
  • Install the Anthropic SDK and configure your API key.
  • Create the scanner file and a deliberately vulnerable test target.
Verify Node.js and initialize the project

The Anthropic SDK requires Node.js 20 or newer. A quick version check confirms you are ready to go.

  • Confirm your Node.js version by running this command:
node -v

You should see v20 or higher (for example v24.18.1).

Seeing a version below 20 or command not found?

Download the latest LTS release from nodejs.org and install it. After installing, close and reopen your terminal, then run node -v again.

help me install Node.js v20 or newer on my machine

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir security-scanner
cd security-scanner
  • Initialize a new Node.js project by running:
npm init -y

You should see output confirming a package.json file was created in your project folder.

What does npm init -y do?

This command creates a package.json file with sensible defaults. The -y flag skips the interactive prompts and accepts all defaults automatically.

The default package.json uses CommonJS modules, but the Anthropic SDK and Chalk both use ES module imports. You need to replace the file contents to enable ES modules.

  • Open package.json in your code editor and replace its entire contents with:
{
  "name": "security-scanner",
  "version": "1.0.0",
  "description": "AI-powered CLI security scanner using Claude",
  "main": "scanner.js",
  "type": "module",
  "scripts": {
    "scan": "node scanner.js"
  },
  "keywords": ["security", "scanner", "cli", "claude"],
  "license": "MIT"
}

Why add type module?

Setting "type": "module" tells Node.js to treat every .js file in this project as an ES module. This lets you use import and export syntax instead of require().

The Anthropic SDK publishes its examples using import syntax, and Chalk (used in the Secret Mission) is ESM-only. Configuring this up front avoids module compatibility issues later.

  • Save package.json.
Install the Anthropic SDK and set your API key

The @anthropic-ai/sdk package handles authentication and request formatting for Claude's API. Once installed, you also need to expose your API key as an environment variable so the SDK can find it.

  • Install the Anthropic SDK by running:
npm install @anthropic-ai/sdk

You should see output showing packages added and an install confirmation. The dependencies field is now added to your package.json automatically.

Seeing install errors?

Make sure you are inside the security-scanner folder (run pwd to check). Also confirm your Node.js version is v20 or higher, as older versions may fail to resolve the SDK.

help me fix npm install errors for @anthropic-ai/sdk

The SDK reads your API key from the ANTHROPIC_API_KEY environment variable by default. You need to export it in your current terminal session.

  • Set your API key for this terminal session by running:

macOS

export ANTHROPIC_API_KEY=your-api-key-here

Windows

$env:ANTHROPIC_API_KEY = "your-api-key-here"
  • Replace your-api-key-here with the actual API key from your Anthropic account.

How do I create an API key?

If you do not have an Anthropic API key yet, create one in the Anthropic Console.

  • Sign in at console.anthropic.com (or create a free account).
  • Go to Settings then API keys.
  • Click Create Key and give it a name (for example security-scanner).
  • Copy the key immediately. It starts with sk-ant- and is only shown once.

💡 Why not a .env file?

Exporting directly in the terminal keeps your key out of any file that could accidentally be committed to Git. The SDK picks up ANTHROPIC_API_KEY automatically with no extra code needed.

This export only lasts for your current terminal session. If you close the terminal, you will need to export it again.

Create the project files

Your project needs two files: scanner.js (where you will build the scanner) and sample-vulnerable.js (a deliberately insecure file for testing).

  • Create the empty scanner file by running:
echo "// empty placeholder file" > scanner.js
  • Confirm both package.json and scanner.js exist by running:
ls

You should see node_modules, package-lock.json, package.json, and scanner.js listed.

Now you need the test target. This file contains four common security vulnerabilities that your scanner will learn to detect.

  • Create a new file called sample-vulnerable.js in your code editor and paste the following setup code:
const express = require('express');
const app = express();
const { exec } = require('child_process');

// Hardcoded credentials
const DB_PASSWORD = 'super_secret_123';
const API_KEY = 'sk-1234567890abcdef';

What does this setup code do?

This imports Express (a web framework) and Node's exec function for running shell commands. The hardcoded credentials on lines 6-7 are the first vulnerability. Real apps should never store secrets directly in source code.

  • Below the code you just pasted, add the three vulnerable route handlers:
app.get('/user', (req, res) => {
  // SQL Injection vulnerability
  const userId = req.query.id;
  const query = `SELECT * FROM users WHERE id = '${userId}'`;

  connection.query(query, (err, results) => {
    res.json(results);
  });
});

app.get('/run', (req, res) => {
  // Command Injection vulnerability
  const filename = req.query.file;
  exec(`cat ${filename}`, (err, stdout) => {
    res.send(stdout);
  });
});

app.get('/page', (req, res) => {
  // Cross-Site Scripting (XSS) vulnerability
  const name = req.query.name;
  res.send(`<h1>Hello, ${name}!</h1>`);
});

app.listen(3000);

What vulnerabilities are in these routes?

  • The /user route has SQL injection. User input is inserted directly into a database query with no sanitization.
  • The /run route has command injection. An attacker could pass ; rm -rf / as the filename.
  • The /page route has cross-site scripting (XSS). User input is rendered as raw HTML with no escaping.
  • Save sample-vulnerable.js.

✔️ Awesome, I've got everything!

Great. Double check you have saved sample-vulnerable.js and that it contains both the setup section and the three route handlers.

ⓧ I'd like to double check the full code

Scroll up and confirm these two chunks are present in your sample-vulnerable.js file, in this order:

  • The setup chunk: require imports, DB_PASSWORD, and API_KEY constants.
  • The routes chunk: three app.get handlers (/user, /run, /page) followed by app.listen(3000).

File not showing up in your editor?

Make sure your editor has the security-scanner folder open. In VS Code, click File then Open Folder and select the security-scanner folder on your Desktop.

help me find and open my security-scanner project folder

Verify the setup

Before you run this next command, predict: do you think Node.js will be able to import the Anthropic SDK successfully? What could go wrong?

  • Test that ES modules and the SDK are working together by running:
node -e "import Anthropic from '@anthropic-ai/sdk'; console.log('SDK loaded')"

You should see SDK loaded printed to your terminal. This confirms Node.js can resolve ES module imports and the Anthropic SDK is installed correctly.

Seeing a module or import error?

  • If you see a warning about ES modules, confirm your package.json contains "type": "module". Without it, Node.js defaults to CommonJS and rejects import statements.
  • If you see a "Cannot find package" error, run ls node_modules/@anthropic-ai to check the SDK installed. If the folder is missing, run npm install @anthropic-ai/sdk again.

help me fix the import error when running my Node.js security scanner setup

Your project foundation is solid. Next up, you will write the scanner logic that reads a file from disk, sends it to Claude, and gets back a raw security analysis.

Read Code and Get a Raw Security Analysis

Your scanner project is set up, but scanner.js is still an empty file.

Before worrying about structured output or pretty formatting, you need to prove the end-to-end flow works: read a file from disk, send it to Claude, and see a security analysis come back.

In this step, get ready to:
  • Build a CLI entry point that validates the file path, extension, and API key.
  • Read the target file from disk with error handling.
  • Send the code to Claude and see a raw security analysis.
Build the CLI entry point with input validation

A good CLI tool catches bad input early, before doing any expensive work like API calls. Your scanner needs to check three things up front: that a file path was provided, that the file is a supported type, and that the API key is available.

  • Open scanner.js in your editor and replace its contents with the imports, constants, and validation function:
import Anthropic from '@anthropic-ai/sdk';
import { readFile } from 'node:fs/promises';
import { resolve, extname } from 'node:path';

// Only scan JavaScript and TypeScript files
const SUPPORTED_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx'];

function validateInputs(filePath) {
  // Check if a file path was provided
  if (!filePath) {
    console.error('Usage: node scanner.js <file-path>');
    console.error('Example: node scanner.js ./sample-vulnerable.js');
    process.exit(1);
  }

  // Check if the file extension is supported
  const ext = extname(filePath).toLowerCase();
  if (!SUPPORTED_EXTENSIONS.includes(ext)) {
    console.error(`Error: Unsupported file extension "${ext}".`);
    console.error(`Supported extensions: ${SUPPORTED_EXTENSIONS.join(', ')}`);
    process.exit(1);
  }

  // Check if the API key is available
  if (!process.env.ANTHROPIC_API_KEY) {
    console.error('Error: ANTHROPIC_API_KEY environment variable is not set.');
    console.error('Run: export ANTHROPIC_API_KEY=your-key-here');
    process.exit(1);
  }
}

What does this code do?

  • The three imports bring in the Anthropic SDK, Node's file-reading utility, and path helpers.
  • SUPPORTED_EXTENSIONS is an allowlist preventing the scanner from wasting API credits on binary or non-code files.
  • validateInputs() runs three checks in order. If any fail, it prints a helpful error and exits before any async work begins.
  • Now add the main() function at the bottom of scanner.js, below the validateInputs function:
async function main() {
  // Read the file path from the command line argument
  const filePath = process.argv[2];
  validateInputs(filePath);
}

main();

What does this code do?

process.argv[2] grabs the first argument the user types after node scanner.js. The main() call at the bottom kicks off the async flow.

  • Save scanner.js.
  • Test the validation by running the scanner with no arguments:
node scanner.js

You should see the usage message printed to the terminal:

Usage: node scanner.js <file-path>
Example: node scanner.js ./sample-vulnerable.js

Seeing a syntax error instead?

  • Check that your package.json still has "type": "module". Without it, Node.js rejects import statements.
  • Make sure you saved the file after pasting the code.
  • help me debug a syntax error when running scanner.js
Read the file with comprehensive error handling

Now that inputs are validated, the scanner needs to actually read the target file from disk. Several things can go wrong here: the file might not exist, you might lack permissions, or the file could be empty.

  • In scanner.js, add the readSourceFile function between the validateInputs function and the main function:
async function readSourceFile(filePath) {
  const absolutePath = resolve(filePath);

  // Attempt to read the file, handling common errors
  let code;
  try {
    code = await readFile(absolutePath, 'utf-8');
  } catch (error) {
    if (error.code === 'ENOENT') {
      console.error(`Error: File not found: ${absolutePath}`);
    } else if (error.code === 'EACCES') {
      console.error(`Error: Permission denied: ${absolutePath}`);
    } else {
      console.error(`Error reading file: ${error.message}`);
    }
    process.exit(1);
  }

  // Guard against empty files that would waste API credits
  if (code.trim().length === 0) {
    console.error(`Error: File is empty: ${absolutePath}`);
    process.exit(1);
  }

  console.log(`Scanning: ${absolutePath}`);
  console.log(`File size: ${code.length} characters\n`);

  return code;
}

What does this code do?

  • resolve() converts a relative path like ./sample-vulnerable.js into an absolute path for clearer error messages.
  • The try/catch handles two specific error codes: ENOENT (file not found) and EACCES (permission denied).
  • The empty-file check prevents sending blank content to Claude, which would still cost tokens for the prompt wrapper.
  • In the main function, find this line:
  validateInputs(filePath);
  • Add this line directly below it to call your new file-reading function:
  const code = await readSourceFile(filePath);
  • Save scanner.js and run it against the sample file:
node scanner.js ./sample-vulnerable.js

You should see output confirming the file was read successfully, showing the absolute path and character count.

Seeing 'File not found'?

  • Make sure you're running the command from your project directory (the one containing both scanner.js and sample-vulnerable.js).
  • Check the file name matches exactly: sample-vulnerable.js.
  • help me debug a file not found error in my scanner
Send the code to Claude with a security analysis prompt

The file contents are loaded. Now you need to send them to Claude with a prompt that asks for a structured security review.

  • In scanner.js, add the analyzeCode function between readSourceFile and main:
async function analyzeCode(code) {
  const client = new Anthropic();

  const prompt = `You are a security code reviewer. Analyze the following JavaScript/TypeScript code for security vulnerabilities.

Return your findings as a JSON array. Each finding should have these fields:
- "vulnerability": A short name for the vulnerability (e.g., "SQL Injection", "XSS")
- "severity": One of "CRITICAL", "HIGH", "MEDIUM", or "LOW"
- "line": The approximate line number where the issue occurs
- "explanation": A clear explanation of why this is a security risk
- "fix": A recommended code fix or mitigation

If no vulnerabilities are found, return an empty array: []

Respond ONLY with the JSON array, no additional text or markdown formatting.

Code to analyze:
\`\`\`
${code}
\`\`\``;

  const message = await client.messages.create({
    model: 'claude-haiku-4-5',
    max_tokens: 4096,
    messages: [{ role: 'user', content: prompt }],
  });

  return message.content[0].text;
}

What does this code do?

  • new Anthropic() creates a client that automatically reads your ANTHROPIC_API_KEY environment variable.
  • The prompt instructs Claude to return findings as a JSON array with specific fields: vulnerability name, severity level, line number, explanation, and fix.
  • The model claude-haiku-4-5 is fast and inexpensive at $1 per million input tokens. A single scan costs well under $0.01.
  • message.content[0].text extracts the text response from Claude's reply structure.

💡 Why ask for JSON in the prompt?

Asking Claude for structured JSON output means you can parse and format the results programmatically in the next steps. Without this instruction, Claude would return freeform paragraphs that are harder to work with.

  • In the main function, find this line:
  const code = await readSourceFile(filePath);
  • Add these two lines directly below it to call the analysis function and print the raw result:
  const responseText = await analyzeCode(code);
  console.log(responseText);
  • Save scanner.js.

Before you run this, what do you think Claude will return? A clean JSON array, or something with extra formatting around it?

  • Run the scanner against the sample file:
node scanner.js ./sample-vulnerable.js

You should see Claude's raw text analysis printed to the terminal, identifying vulnerabilities like SQL injection, command injection, XSS, and hardcoded credentials in the sample file.

What should I see?

The response is Claude's raw text output. It likely contains a JSON array with vulnerability findings, but it may also include markdown code fences or extra explanation text around the JSON. That inconsistency is a real-world problem you will solve in the next step.

🙋‍♀️ Getting an authentication error?

  • Make sure your ANTHROPIC_API_KEY environment variable is still set in the same terminal session. Run echo $ANTHROPIC_API_KEY to check.
  • If you opened a new terminal tab, you need to export the key again.
  • help me fix an API authentication error with the Anthropic SDK

✔️ Awesome, I've got everything!

Great. Double check you have saved scanner.js and that running node scanner.js ./sample-vulnerable.js prints Claude's analysis.

ⓧ I'd like to double check the full code

Your scanner.js should contain these sections in order. Scroll up and confirm each one is present:

  • Three import statements at the top (Anthropic, readFile, resolve/extname).
  • The SUPPORTED_EXTENSIONS array constant.
  • The validateInputs(filePath) function with three checks.
  • The readSourceFile(filePath) async function with try/catch and empty-file guard.
  • The analyzeCode(code) async function with the prompt and API call.
  • The main() async function calling validateInputs, readSourceFile, analyzeCode, and console.log(responseText).
  • main(); at the very bottom to start the program.

Your scanner can now read any JavaScript or TypeScript file and get Claude's security opinion on it. Next up, you will turn that raw text into reliable structured data you can actually work with.

Extract Structured JSON from Claude's Response

In the previous step, you proved the full pipeline works. Claude reads your code and returns a detailed security analysis. The problem is that response is just a raw text string.

To do anything useful with the results (filter, sort, format), you need to parse that text into structured JSON. This step tackles a real-world LLM integration problem: Claude's output is not always clean JSON, even when you ask nicely.

In this step, get ready to:
  • Attempt to parse Claude's raw response directly and see why it breaks.
  • Build a robust JSON extraction function with multiple fallback strategies.
  • Validate the parsed findings to ensure they have all required fields.
Attempt direct JSON.parse and observe the failure

The simplest approach to parsing Claude's response would be calling JSON.parse() on it directly. Let's try that and see what happens.

  • In scanner.js, find these lines at the end of your main() function:
  const responseText = await analyzeCode(code);
  console.log(responseText);
  • Replace console.log(responseText); with a JSON.parse() call, so those lines now look like this:
  const responseText = await analyzeCode(code);
  const findings = JSON.parse(responseText);
  console.log(findings);
  • Save scanner.js.

Before you run this, do you think the parse will succeed or fail? Think about what Claude's raw output looked like in the previous step.

  • Run the scanner against your sample file:
node scanner.js ./sample-vulnerable.js

You'll see a SyntaxError: Unexpected token crash. That's the designed obstacle for this step.

Why does JSON.parse fail here?

Even though your prompt says "Respond ONLY with the JSON array", Claude frequently wraps its response in markdown code fences like ```json ... ```. That extra wrapping is not valid JSON, so JSON.parse() chokes on it.

This is a real-world LLM integration problem. You can never fully trust the output format, so you need a robust extraction function with fallback strategies.

Build the extractJSON function with fallback strategies

Instead of relying on a single fragile parse, you'll build a function that tries three strategies in order until one succeeds.

  • In scanner.js, add the following extractJSON function above your main() function:
function extractJSON(text) {
  try {
    const parsed = JSON.parse(text);
    if (Array.isArray(parsed)) return parsed;
  } catch {
    // Direct parse failed, try fallbacks
  }

  const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
  if (fenceMatch) {
    try {
      const parsed = JSON.parse(fenceMatch[1].trim());
      if (Array.isArray(parsed)) return parsed;
    } catch {
      // Fence extraction failed, try next fallback
    }
  }

  const arrayMatch = text.match(/\[[\s\S]*\]/);
  if (arrayMatch) {
    try {
      const parsed = JSON.parse(arrayMatch[0]);
      if (Array.isArray(parsed)) return parsed;
    } catch {
      // Array extraction failed
    }
  }

  throw new Error('Could not extract valid JSON array from Claude response.');
}

What does this code do?

  • Strategy 1: Try JSON.parse() directly. If Claude cooperates and returns clean JSON, this succeeds immediately.
  • Strategy 2: Use regex /```(?:json)?\s*\n?([\s\S]*?)\n?```/ to strip markdown code fences. This handles the most common LLM formatting quirk.
  • Strategy 3: Extract any JSON array pattern with /\[[\s\S]*\]/. This catches cases where Claude adds extra text before or after the array.
  • If all three fail, throw a clear error so you know extraction was the problem.
  • Now update your main() function to use extractJSON instead of the broken JSON.parse. Find these lines:
  const findings = JSON.parse(responseText);
  console.log(findings);
  • Replace them with:
  const rawFindings = extractJSON(responseText);
  console.log(rawFindings);
  • Save scanner.js and run the scanner again:
node scanner.js ./sample-vulnerable.js

This time, instead of a crash, you'll see a raw JavaScript array printed to the terminal. The extraction function stripped away the markdown fences and parsed the JSON successfully.

Still seeing a SyntaxError?

  • Make sure you replaced both lines in main() (the JSON.parse line AND the console.log(findings) line).
  • Check that extractJSON is defined above main() in the file. Functions must be declared before they are called in the same scope.
  • Verify the regex strings are copied exactly. The backslashes and character classes are easy to mistype.

Help me debug my extractJSON function

Validate the parsed structure

Extraction alone is not enough. Claude might return array items missing some fields, which would crash your report formatter later. A validation layer filters out any malformed findings before they reach the display code.

  • In scanner.js, add the following validateFindings function below extractJSON and above main():
function validateFindings(findings) {
  const requiredFields = ['vulnerability', 'severity', 'line', 'explanation', 'fix'];
  return findings.filter((finding) => {
    return requiredFields.every((field) => field in finding);
  });
}

What does this code do?

  • requiredFields lists the five fields every finding must have: vulnerability, severity, line, explanation, and fix.
  • The .filter() keeps only findings where all five fields are present. Incomplete items are silently dropped rather than crashing the report.
  • Now update main() one final time. Find these lines:
  const rawFindings = extractJSON(responseText);
  console.log(rawFindings);
  • Replace them with the final wiring that validates findings and logs the count:
  const rawFindings = extractJSON(responseText);
  const findings = validateFindings(rawFindings);
  console.log(`Parsed ${findings.length} findings successfully.\n`);
  • Save scanner.js.

Before you run this, how many findings do you think Claude will identify in the sample file? There are four intentional vulnerabilities in the code.

  • Run the scanner to confirm everything works end-to-end:
node scanner.js ./sample-vulnerable.js

You should see output like Parsed 4 findings successfully. (or a similar count). No more crash, no more raw text wall.

Seeing 'Could not extract valid JSON array'?

  • This means all three extraction strategies failed. Check that your regex patterns in extractJSON are copied exactly, including the backtick characters and escape sequences.
  • Run the scanner once with a temporary console.log(responseText) before the extractJSON call to inspect what Claude actually returned.

Help me debug my JSON extraction

✔️ Awesome, I've got everything!

Double check you've saved scanner.js and that your terminal shows the parsed findings count.

ⓧ I'd like to double check the full code

Your scanner.js should now contain these sections in order. Scroll through your file and confirm each one is present:

  • Import statements (Anthropic, readFile, resolve, extname).
  • SUPPORTED_EXTENSIONS constant.
  • validateInputs() function.
  • readSourceFile() function.
  • analyzeCode() function.
  • extractJSON() function with three fallback strategies.
  • validateFindings() function.
  • main() function calling extractJSON and validateFindings, ending with console.log(`Parsed ${findings.length} findings successfully.\n`).
  • main(); call at the bottom.

Your scanner now handles messy LLM output gracefully. Next up, you'll turn that parsed data into a formatted security report that's actually pleasant to read in the terminal.

Format and Display the Security Report

Your scanner can read files, send them to Claude, and extract structured JSON from the response. Right now, those findings print as a raw "Parsed X findings successfully" message.

Raw JSON is hard to scan quickly when you want to know what's wrong with your code. In this step, you'll format the findings into a clean, numbered terminal report that shows severity, line numbers, explanations, and fixes at a glance.

In this step, get ready to:
  • Build a displayReport function that formats findings into a readable report.
  • Add error handling for API failures like invalid keys and rate limits.
  • Run the complete scanner against your vulnerable sample file.
Build the displayReport function

The report function needs to handle two cases. If Claude finds no issues, it prints a clean "all clear" message. If findings exist, it loops through each one and prints a numbered entry with all the details.

  • In scanner.js, find the closing brace of the validateFindings function.
  • Add the following displayReport function below it:
function displayReport(findings) {
  if (findings.length === 0) {
    console.log('No security vulnerabilities found!');
    return;
  }

  console.log(`Found ${findings.length} potential vulnerability${findings.length === 1 ? '' : 'ies'}:\n`);
  console.log('='.repeat(60));

  findings.forEach((finding, index) => {
    console.log(`\n[${index + 1}] ${finding.vulnerability}`);
    console.log(`    Severity: ${finding.severity}`);
    console.log(`    Line: ~${finding.line}`);
    console.log(`    Explanation: ${finding.explanation}`);
    console.log(`    Fix: ${finding.fix}`);
    console.log('-'.repeat(60));
  });
}

What does this code do?

The function first handles the empty case. If Claude found nothing wrong, the scanner prints a reassuring message and exits early.

For actual findings, it prints a header with the total count, then a visual divider of equals signs. Each finding gets a numbered entry showing the vulnerability name, severity level, approximate line number, an explanation, and a recommended fix. Dashes separate each entry for readability.

  • Save scanner.js.

The function is defined but not called yet. The next chunk wires it into your main flow.

Wire everything together with error handling

Your current main function has no error handling for API failures. If your key expires, the rate limit kicks in, or your network drops, the scanner crashes with an unhelpful stack trace. You need a top-level try/catch that catches these cases and prints a human-friendly message.

  • In scanner.js, find the existing main function. It currently looks like this:
async function main() {
  const filePath = process.argv[2];

  validateInputs(filePath);

  const code = await readSourceFile(filePath);
  const responseText = await analyzeCode(code);
  const rawFindings = extractJSON(responseText);
  const findings = validateFindings(rawFindings);
  console.log(`Parsed ${findings.length} findings successfully.\n`);
}

main();
  • Replace the entire main function and the main() call with this updated version:
async function main() {
  const filePath = process.argv[2];

  validateInputs(filePath);

  try {
    const code = await readSourceFile(filePath);
    const responseText = await analyzeCode(code);
    const rawFindings = extractJSON(responseText);
    const findings = validateFindings(rawFindings);
    console.log(`Parsed ${findings.length} findings successfully.\n`);
    displayReport(findings);
  } catch (error) {
    if (error.status === 401) {
      console.error('Error: Invalid API key. Check your ANTHROPIC_API_KEY environment variable.');
    } else if (error.status === 429) {
      console.error('Error: Rate limit exceeded. Please wait a moment and try again.');
    } else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
      console.error('Error: Network error. Check your internet connection.');
    } else {
      console.error(`Error: ${error.message}`);
    }
    process.exit(1);
  }
}

main();

What changed here?

  • The async operations are now wrapped in a try/catch block. The Anthropic SDK throws errors with a status property for HTTP errors, so you can check for specific status codes.
  • Status 401 means the API key is invalid. Status 429 means you've hit the rate limit.
  • Network errors surface via Node.js error codes (ECONNREFUSED, ENOTFOUND) rather than HTTP status codes.
  • After the extraction and validation, displayReport(findings) now prints the formatted output to the terminal.
  • Save scanner.js.

Before you run the scanner, think about what you expect to see. The sample file has SQL injection, command injection, XSS, and hardcoded credentials. How many findings do you think Claude will report?

  • Run the complete scanner against your sample file:
node scanner.js ./sample-vulnerable.js

You should see a numbered security report with findings separated by dashes. Each entry shows the vulnerability name, severity rating, approximate line number, an explanation, and a fix recommendation.

What should I see?

Claude typically identifies 4-5 vulnerabilities in the sample file: SQL injection, command injection, XSS, and hardcoded credentials. The exact wording and severity ratings may vary slightly between runs since Claude's responses are non-deterministic.

The important thing is that you see a clean, numbered report with dividers between findings. Not raw JSON.

🙋‍♀️ Seeing an error instead of a report?

  • If you see "Invalid API key", confirm your ANTHROPIC_API_KEY environment variable is still set by running echo $ANTHROPIC_API_KEY in your terminal. If it's empty, re-export it.
  • If you see "Rate limit exceeded", wait 30 seconds and try again.
  • If you see a syntax error, make sure the displayReport function is placed between validateFindings and main. Functions must be defined before they're called.

Still stuck? Help me debug my security scanner output.

✔️ Awesome, I've got everything!

Great. Double check you've saved scanner.js and your report printed cleanly.

ⓧ I'd like to double check the full code

Your scanner.js is now quite long. Scroll through and confirm these sections appear in this order:

  • Imports (Anthropic, readFile, resolve, extname) and the SUPPORTED_EXTENSIONS array.
  • validateInputs function (checks file path, extension, and API key).
  • readSourceFile function (reads file with error handling).
  • analyzeCode function (sends code to Claude).
  • extractJSON function (parses JSON with fallbacks).
  • validateFindings function (filters incomplete findings).
  • displayReport function (formats and prints the report).
  • main function with try/catch wrapping all async calls, followed by the main() call.

Your security scanner is complete. You can now point it at any JavaScript or TypeScript file and get a structured vulnerability report powered by Claude. In the Secret Mission, you'll level it up with color-coded severity output and sorted findings.

Secret mission

Add Color-Coded Severity with Chalk

Your scanner works, but a plain text report treats CRITICAL and LOW findings the same visually. In this mission, you'll add color-coded severity output so dangerous vulnerabilities jump off the screen, sort findings from most to least severe, and display a summary line showing counts per severity level.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally and the Anthropic API is pay-per-use, so there are no ongoing costs when you are not running scans.

Resources you used:

  • The security-scanner project folder on your Desktop (includes scanner.js, sample-vulnerable.js, package.json, and node_modules/).
  • The ANTHROPIC_API_KEY environment variable exported in your terminal session.

Keep everything running

No action needed. Choose this if you plan to keep scanning files or extending the tool.

  • Your project folder stays on the Desktop, ready to use anytime with node scanner.js <file>.
  • The ANTHROPIC_API_KEY environment variable only lasts for the current terminal session. If you close the terminal, you will need to export it again next time.
  • You are only charged when you actively run scans. No background processes are running.

Pause - I'll come back to this later

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

  • Close the terminal window. This removes the ANTHROPIC_API_KEY variable from memory.
  • Your security-scanner folder and all its files remain on your Desktop.
  • When you return, open a new terminal, export your API key again, and you are ready to scan.

Delete - I don't want to use this again

Remove all project files and clear the environment variable from your current session.

  • Clear the environment variable from your current terminal session by running:
unset ANTHROPIC_API_KEY
  • Delete the entire project folder by running:

macOS

rm -rf ~/Desktop/security-scanner

Windows

Remove-Item -Recurse -Force ~\Desktop\security-scanner
  • Confirm the folder is gone by running:
ls ~/Desktop

You should no longer see security-scanner in the output.

Nice Work!

Nice Work!

You just built a fully functional CLI security scanner that turns Claude into your personal code reviewer. Point it at any JavaScript or TypeScript file, and within seconds you get a structured vulnerability report with severity ratings and fix recommendations.

You've learned how to:

  • Build a CLI tool that validates inputs, reads source files, and integrates with the Anthropic SDK to send code to Claude Haiku 4.5 for security analysis.
  • Implement robust JSON extraction with multiple fallback strategies that handle markdown code fences and other LLM output quirks reliably.
  • Format structured findings into a readable terminal report with severity ratings, line numbers, and actionable fix recommendations.
  • Secret Mission: Added Chalk-based color-coded severity output, sorted findings from critical to low, and displayed a severity summary.

Ready to quiz yourself?