Linux C Vulnerability Scanner with Claude

Build a CLI tool that uses Claude AI to scan C code for security vulnerabilities.

Introduction

30 Second Summary

Security vulnerabilities in C code are behind some of the most damaging software exploits in history. Traditional scanning tools rely on pattern matching. They catch strcpy but miss the subtle logic bugs that actually get exploited in the wild.

In this project, you will build a Python command-line tool that uses the Claude API to perform deep semantic analysis of C source code for security vulnerabilities. Your scanner will produce structured reports with severity ratings, exploit scenarios, and fix recommendations.

What You'll Build

A CLI vulnerability scanner that reads C source files, sends them to Claude with a security-focused system prompt, and outputs structured JSON vulnerability reports.

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

  • A Python CLI scanner (vulnscan.py) that analyzes C files using Claude's API.
  • Intentionally vulnerable C test files covering buffer overflow, use-after-free, format string, integer overflow, and TOCTOU vulnerabilities.
  • Structured JSON reports with severity ratings, CWE IDs, line numbers, and fix recommendations.
  • Secret Mission: A "fix mode" that generates patched C code with vulnerabilities remediated.

Are there any prerequisites?

This project assumes you have Python experience and a basic understanding of C code. You will also need an Anthropic Console account with pay-as-you-go API credits (this is separate from a Claude Max subscription).

Before We Start

Before We Start...

Set Up Your Environment

Your vulnerability scanner will send C source code to Claude through its API and receive structured security analysis in return. To make those API calls, you need an Anthropic Console account with credits and an API key.

In this step, you'll create your account, set up a Python project directory with the Anthropic SDK installed, and confirm everything connects with a quick test call.

In this step, get ready to:
  • Create an Anthropic Console account and generate an API key.
  • Set up a project directory with a Python virtual environment and the Anthropic SDK.
  • Verify API connectivity with a test call to Claude.
Create your Anthropic Console account and API key

The Anthropic Console is where you manage API access, billing, and keys. This is separate from a Claude chat subscription.

  • Open console.anthropic.com in your browser.
  • Click Sign Up and create your account with an email address or Google login.
  • Once signed in, navigate to Billing in the left sidebar.
  • Add a payment method and load at least $5 in credits.

How much will this project cost?

Claude Sonnet 4.6 costs $3 per million input tokens and $15 per million output tokens. A typical C file scan uses around 2,000-8,000 tokens, so each scan costs well under $0.05.

Your total project cost will be around $0.10-$0.50. New Console accounts may receive a small amount of free credits to start.

Now generate your API key.

  • Click API Keys in the left sidebar.
  • Click Create Key.
  • Give your key a name like vulnscan.
  • Copy the key immediately. You will not be able to see it again.

Keep your API key secret

Your API key is a credential that grants access to your Anthropic account and its credits. Never commit it to Git, share it publicly, or paste it into files that others can see.

Your scanner needs access to this key at runtime. The Anthropic SDK automatically reads it from an environment variable called ANTHROPIC_API_KEY.

  • Press Cmd+Space (macOS) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Set the API key in your current shell session by running this command (replace your-api-key-here with the key you just copied):
export ANTHROPIC_API_KEY='your-api-key-here'
  • Persist the key across terminal sessions by adding it to your ~/.zshrc file:
echo "export ANTHROPIC_API_KEY='your-api-key-here'" >> ~/.zshrc

What does this do?

The first command sets the variable for your current terminal session only. The second command appends it to ~/.zshrc, which is a config file that runs every time you open a new terminal window on macOS.

This means you only need to do this setup once. Every future terminal session will have access to the key automatically.

  • Confirm the variable is set by running:
echo $ANTHROPIC_API_KEY

You should see your API key printed back in the terminal.

Seeing a blank line instead of your key?

Make sure you ran the export command in the same terminal window where you are running echo. If you opened a new tab or window, you need to run source ~/.zshrc first or re-run the export command.

help me debug why my ANTHROPIC_API_KEY environment variable is empty

Create your project directory and install the SDK

Your scanner will live in its own directory with a Python virtual environment. A virtual environment keeps your project's packages isolated from other Python projects on your machine.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project directory and move into it by running these commands:
mkdir vulnscan
cd vulnscan
  • Create a Python virtual environment and activate it by running:
python3 -m venv venv
source venv/bin/activate

Your terminal prompt should now show (venv) at the beginning, confirming the virtual environment is active.

What is a virtual environment?

A virtual environment is a self-contained Python installation inside your project folder. Packages you install with pip go into this environment only, not your system-wide Python.

This prevents version conflicts between projects. You activate it each time you work on this project with source venv/bin/activate.

  • Install the Anthropic Python SDK by running:
pip install anthropic

You should see pip download and install the anthropic package along with its dependencies. The final line should say Successfully installed.

Seeing 'pip: command not found'?

Make sure your virtual environment is active. You should see (venv) in your terminal prompt. If not, run source venv/bin/activate again from inside the vulnscan directory.

help me fix my pip install issue

Verify your API connection

Before building the full scanner, confirm that your API key and SDK are working together. You will send a simple message to Claude and check that a response comes back.

  • Run this test command to send a message to Claude:
python3 << 'EOF'
import anthropic

client = anthropic.Anthropic()
message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=100,
    messages=[{"role": "user", "content": "Say hello in one sentence."}]
)
print(message.content[0].text)
EOF

What does this test do?

This creates an Anthropic client (which reads your ANTHROPIC_API_KEY environment variable automatically), sends a simple message to claude-sonnet-4-6, and prints the response text.

If everything is configured correctly, Claude will respond with a short greeting.

You should see a one-sentence greeting from Claude printed in your terminal.

Seeing an authentication error?

If you see AuthenticationError, your API key is not being read correctly. Run echo $ANTHROPIC_API_KEY to check that the key is still set in this terminal session.

If you see PermissionDeniedError or a billing error, go back to console.anthropic.com and confirm you have credits loaded under Billing.

help me debug my Claude API connection

Your environment is ready. You have a working API connection and a clean project directory to build in. Next up, you'll create a set of intentionally vulnerable C files to test your scanner against.

Create Vulnerable C Test Files

Your scanner needs something to scan. Before building the tool itself, you'll create a set of intentionally vulnerable C files that represent real-world security bugs found in Linux systems.

These files cover five major vulnerability classes that cause real CVEs in production code. Each file contains one or two intentional bugs that your scanner should catch when you run it in a later step.

In this step, get ready to:
  • Create a samples/ directory for your test files.
  • Write five intentionally vulnerable C files covering buffer overflow, use-after-free, format string, integer overflow, and TOCTOU vulnerabilities.
  • Create a safe baseline file for comparison testing.
Create the samples directory and buffer overflow file

A buffer overflow happens when a program writes data past the end of a memory buffer. It is one of the oldest and most exploited vulnerability classes in C, classified as CWE-120.

  • Make sure your terminal is inside your vulnscan project folder, then create the samples/ directory by running this command:
mkdir samples
  • Create a new file called buffer_overflow.c inside the samples/ folder and paste the following code:
#include <stdio.h>
#include <string.h>

void process_input(char *user_input) {
    char buffer[64];
    // Vulnerable: no bounds checking on copy
    strcpy(buffer, user_input);
    printf("Processed: %s\n", buffer);
}

void off_by_one_example() {
    char name[32];
    int i;
    // Vulnerable: off-by-one, writes past buffer end
    for (i = 0; i <= 32; i++) {
        name[i] = 'A';
    }
    name[31] = '\0';
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s <input>\n", argv[0]);
        return 1;
    }
    process_input(argv[1]);
    off_by_one_example();
    return 0;
}

What vulnerabilities are planted here?

This file contains two classic buffer overflow bugs:

  • strcpy(buffer, user_input) copies user input into a 64-byte stack buffer with no bounds checking. An attacker can provide input longer than 64 bytes to overwrite the return address and execute arbitrary code.
  • The for (i = 0; i <= 32; i++) loop writes 33 bytes into a 32-byte buffer. This off-by-one error corrupts the byte immediately after the buffer.
  • Save buffer_overflow.c.
  • Confirm the file was created by running:
ls samples/

You should see buffer_overflow.c listed in the output.

Don't see the file?

Make sure you created the file inside the samples/ directory, not in the root vulnscan/ folder. Check that you ran mkdir samples first.

Help me find where my file was created

Create the use-after-free and format string files

A use-after-free bug (CWE-416) occurs when a program continues using a pointer after the memory it points to has been freed. A format string vulnerability (CWE-134) occurs when user-controlled input is passed directly as a format string to printf-family functions.

  • Create a new file called use_after_free.c inside samples/ and paste this code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char *name;
    int id;
    void (*print_func)(const char *);
} User;

void print_user(const char *name) {
    printf("User: %s\n", name);
}

User *create_user(const char *name, int id) {
    User *user = (User *)malloc(sizeof(User));
    if (!user) return NULL;
    user->name = strdup(name);
    user->id = id;
    user->print_func = print_user;
    return user;
}

void delete_user(User *user) {
    free(user->name);
    free(user);
    // Vulnerable: caller still holds pointer after free
}

int main() {
    User *admin = create_user("admin", 1);
    User *guest = create_user("guest", 2);

    delete_user(admin);

    // Vulnerable: use-after-free - admin pointer is dangling
    printf("Admin ID: %d\n", admin->id);
    admin->print_func(admin->name);

    delete_user(guest);
    return 0;
}

What vulnerabilities are planted here?

After delete_user(admin) is called, the admin pointer becomes dangling. The code then reads admin->id and calls admin->print_func on freed memory. An attacker could reclaim that memory with controlled data and hijack the function pointer to execute arbitrary code.

  • Save use_after_free.c.
  • Create another file called format_string.c inside samples/ and paste this code:
#include <stdio.h>
#include <string.h>

void log_message(char *user_msg) {
    char log_buffer[256];
    snprintf(log_buffer, sizeof(log_buffer), "LOG: %s", user_msg);
    // Vulnerable: user-controlled format string
    printf(log_buffer);
    printf("\n");
}

void authenticate(char *username) {
    int is_admin = 0;
    char buffer[128];

    snprintf(buffer, sizeof(buffer), username);
    // Vulnerable: format string can overwrite is_admin via %n

    if (is_admin) {
        printf("Admin access granted!\n");
    } else {
        printf("Access denied for: %s\n", buffer);
    }
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s <message>\n", argv[0]);
        return 1;
    }
    log_message(argv[1]);
    authenticate(argv[1]);
    return 0;
}

What vulnerabilities are planted here?

This file has two format string bugs:

  • printf(log_buffer) passes user-controlled content as a format string. An attacker can inject %x to leak stack data or %n to write to arbitrary memory.
  • snprintf(buffer, sizeof(buffer), username) uses the username directly as a format string. An attacker can overwrite the is_admin variable on the stack via %n to gain admin access.
  • Save format_string.c.
  • Confirm both files exist by running:
ls samples/

You should see buffer_overflow.c, format_string.c, and use_after_free.c listed.

Create the integer overflow, TOCTOU, and safe test files

An integer overflow (CWE-190) happens when arithmetic on fixed-size integers wraps around, causing a large allocation request to become tiny. A TOCTOU race condition (CWE-367) exploits the gap between checking a resource's state and using it.

  • Create integer_overflow.c inside samples/ and paste this code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

void *safe_alloc(uint32_t count, uint32_t element_size) {
    // Vulnerable: multiplication can overflow uint32_t
    uint32_t total_size = count * element_size;
    void *ptr = malloc(total_size);
    return ptr;
}

void process_records(uint32_t num_records) {
    char **records = (char **)safe_alloc(num_records, sizeof(char *));
    if (!records) return;

    for (uint32_t i = 0; i < num_records; i++) {
        records[i] = malloc(64);
        if (records[i]) {
            snprintf(records[i], 64, "Record %u", i);
        }
    }

    for (uint32_t i = 0; i < num_records; i++) {
        free(records[i]);
    }
    free(records);
}

int copy_with_offset(char *dest, size_t dest_size, char *src, int offset) {
    // Vulnerable: signed/unsigned comparison
    if (offset < dest_size) {
        memcpy(dest + offset, src, strlen(src) + 1);
    }
    return 0;
}

int main() {
    process_records(0x40000001);

    char buf[64];
    copy_with_offset(buf, sizeof(buf), "hello", -1);
    return 0;
}

What vulnerabilities are planted here?

This file has two integer-related bugs:

  • safe_alloc multiplies count * element_size as uint32_t. When count is 0x40000001 and element size is 4, the multiplication wraps to 4 bytes. The loop then writes far beyond the tiny allocation.
  • copy_with_offset compares a signed int offset against an unsigned size_t. A negative offset like -1 bypasses the bounds check and writes to memory before the buffer.
  • Save integer_overflow.c.
  • Create toctou.c inside samples/ and paste this code:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int read_config(const char *filepath) {
    struct stat st;

    // Vulnerable: TOCTOU race between stat and open
    if (stat(filepath, &st) != 0) {
        perror("stat");
        return -1;
    }

    if (st.st_uid != 0) {
        fprintf(stderr, "Config file not owned by root!\n");
        return -1;
    }

    // Race window: attacker can replace file between stat() and open()
    FILE *f = fopen(filepath, "r");
    if (!f) {
        perror("fopen");
        return -1;
    }

    char buffer[1024];
    while (fgets(buffer, sizeof(buffer), f)) {
        printf("Config: %s", buffer);
    }

    fclose(f);
    return 0;
}

void write_log(const char *logpath, const char *message) {
    // Vulnerable: TOCTOU with access() before open()
    if (access(logpath, W_OK) == 0) {
        FILE *f = fopen(logpath, "a");
        if (f) {
            fprintf(f, "%s\n", message);
            fclose(f);
        }
    }
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s <config_path>\n", argv[0]);
        return 1;
    }
    read_config(argv[1]);
    write_log("/var/log/app.log", "Application started");
    return 0;
}

What vulnerabilities are planted here?

TOCTOU stands for Time-Of-Check-Time-Of-Use. Both functions have a gap between verifying a condition and acting on it:

  • read_config calls stat() to check file ownership, then fopen() to read it. An attacker can swap the file with a symlink in that window.
  • write_log calls access() to check write permission, then fopen() to append. An attacker can create a symlink to a sensitive file between the check and the open.
  • Save toctou.c.

Finally, you need a clean file with no vulnerabilities. This gives you a baseline to confirm your scanner does not produce false positives on safe code.

  • Create hello_safe.c inside samples/ and paste this code:
#include <stdio.h>

int main() {
    printf("Hello, world!\n");
    return 0;
}
  • Save hello_safe.c.
  • Confirm all six files exist by running:
ls samples/

You should see buffer_overflow.c, format_string.c, hello_safe.c, integer_overflow.c, toctou.c, and use_after_free.c listed.

Missing a file?

Double check that each file is saved inside the samples/ folder (not the parent vulnscan/ folder). Also verify the file names match exactly, including underscores.

Help me find my missing sample files

Why write intentionally buggy code?

These files are your test suite. By planting known vulnerabilities, you can measure whether your scanner correctly identifies them. Real security tools are validated the same way, using curated datasets of known-vulnerable code to benchmark detection rates.

Your test files are ready. Next up, you'll build the scanner that reads these files and sends them to Claude for security analysis.

Build the Scanner Core

You have a set of intentionally vulnerable C files ready to scan. Now you need the tool that actually reads these files and asks Claude to find the bugs.

In this step, you will build vulnscan.py. This is the Python script that reads C source code, sends it to Claude with a security-focused system prompt, and parses the structured vulnerability report that comes back.

In this step, get ready to:
  • Write a system prompt that turns Claude into a security engineer.
  • Build a scan_file() function that sends C code to Claude and gets back JSON findings.
  • Add a CLI interface with argparse for scanning files and directories.
Create vulnscan.py with the scan_file function

The scanner works by reading a C file, wrapping it in a carefully designed prompt, and sending it to Claude. Claude analyzes the code semantically and returns structured JSON describing each vulnerability it finds.

  • Create a new file called vulnscan.py in your vulnscan/ project folder (the same level as your samples/ directory).
  • Add the file header, imports, and system prompt by pasting this code:
#!/usr/bin/env python3
"""Linux C Code Vulnerability Scanner powered by Claude."""

import argparse
import json
import os
import sys
from pathlib import Path

import anthropic

SYSTEM_PROMPT = """You are a senior security engineer specializing in Linux C code review. \
Your task is to analyze C source code for security vulnerabilities.

Focus on these vulnerability classes:
- Buffer overflow (stack and heap): strcpy, strcat, sprintf, gets, scanf without bounds, off-by-one errors
- Use-after-free and double-free: dangling pointers, freed memory access
- Format string vulnerabilities: user input as format strings in printf-family functions
- Integer overflow/underflow: allocation size overflows, signed/unsigned confusion, truncation
- TOCTOU race conditions: access() before open(), stat() before fopen(), symlink attacks
- Privilege escalation: unchecked setuid/seteuid return values, improper privilege dropping
- Race conditions: shared state without locks, check-then-act patterns
- Unsafe library calls: gets, mktemp, tmpnam, alloca with unchecked sizes

For each vulnerability found, provide:
- line: the line number where the vulnerability exists
- severity: CRITICAL, HIGH, MEDIUM, or LOW
- cwe_id: the relevant CWE identifier (e.g., CWE-120, CWE-416, CWE-134)
- category: short category name (e.g., buffer_overflow, use_after_free, format_string)
- description: clear explanation of the vulnerability
- exploit_scenario: how an attacker could exploit this
- fix_recommendation: specific code fix or mitigation

Respond with ONLY a JSON array of findings. If no vulnerabilities are found, respond with an empty array [].
Do not include markdown fences, explanations, or any other text outside the JSON array.

Example output format:
[
  {
    "line": 15,
    "severity": "CRITICAL",
    "cwe_id": "CWE-120",
    "category": "buffer_overflow",
    "description": "strcpy copies user input into fixed-size stack buffer without bounds checking",
    "exploit_scenario": "Attacker provides input longer than 64 bytes to overwrite return address and execute arbitrary code",
    "fix_recommendation": "Replace strcpy with strncpy(buffer, user_input, sizeof(buffer) - 1) and null-terminate"
  }
]"""

What does this code do?

The imports bring in everything the scanner needs: argparse for CLI flags, json for parsing Claude's output, pathlib for file handling, and anthropic for the Claude API client.

The SYSTEM_PROMPT is the instruction set Claude receives with every request. It tells Claude exactly which vulnerability classes to look for, what JSON schema to output, and includes an example so the response format is consistent.

  • Below the SYSTEM_PROMPT, add the ANSI color codes and the scan_file() function:
# ANSI color codes for terminal output
COLORS = {
    "CRITICAL": "\033[91m",  # Red
    "HIGH": "\033[93m",      # Yellow
    "MEDIUM": "\033[96m",    # Cyan
    "LOW": "\033[37m",       # White
    "RESET": "\033[0m",
    "BOLD": "\033[1m",
}


def scan_file(client: anthropic.Anthropic, filepath: str) -> list:
    """Scan a single C file for vulnerabilities using Claude."""
    path = Path(filepath)
    if not path.exists():
        print(f"Error: File not found: {filepath}", file=sys.stderr)
        return []

    code = path.read_text()
    if not code.strip():
        return []

    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=4096,
        system=SYSTEM_PROMPT,
        messages=[
            {
                "role": "user",
                "content": f"Analyze this C source file for security vulnerabilities:\n\nFilename: {path.name}\n\n```c\n{code}\n```",
            }
        ],
    )

    response_text = message.content[0].text.strip()

    # Handle potential markdown fences in response
    if response_text.startswith("```"):
        lines = response_text.split("\n")
        response_text = "\n".join(lines[1:-1])

    try:
        findings = json.loads(response_text)
    except json.JSONDecodeError:
        # Try to extract JSON array from response
        start = response_text.find("[")
        end = response_text.rfind("]") + 1
        if start != -1 and end > start:
            try:
                findings = json.loads(response_text[start:end])
            except json.JSONDecodeError:
                print(f"Warning: Could not parse response for {filepath}", file=sys.stderr)
                return []
        else:
            print(f"Warning: No JSON found in response for {filepath}", file=sys.stderr)
            return []

    # Add filename to each finding
    for finding in findings:
        finding["file"] = str(path.name)

    return findings

What does this code do?

The COLORS dictionary maps severity levels to ANSI escape codes. These let you print colored text in your terminal without any extra libraries.

The scan_file() function does three things: reads the C file, sends it to Claude using client.messages.create() with the system prompt, and parses the JSON response. The fallback parsing logic handles cases where Claude wraps its JSON in markdown fences or adds extra text.

  • Save vulnscan.py.

Seeing a syntax error on the system prompt?

Triple-quoted strings in Python are sensitive to stray quotes. Make sure your SYSTEM_PROMPT starts with """ and ends with """ on its own. The backslash at the end of the first line prevents a leading newline.

help me fix a syntax error in my Python triple-quoted string

Design the security-focused system prompt

The system prompt you just added is the most important part of your scanner. It determines what Claude looks for and how it reports findings.

Why is prompt design so important here?

Unlike pattern-matching tools that grep for dangerous functions like strcpy, your scanner performs semantic analysis. Claude traces data flow through function calls, reasons about race conditions, and understands whether a potentially dangerous pattern is actually exploitable in context.

The prompt achieves this by listing specific vulnerability classes (buffer overflow, use-after-free, format string, integer overflow, TOCTOU), requesting a structured JSON schema with fields like cwe_id and exploit_scenario, and providing a concrete example output. The example anchors Claude's response format so you get parseable JSON every time.

What are CWE IDs?

CWE stands for Common Weakness Enumeration. Each ID (like CWE-120 for buffer overflow or CWE-416 for use-after-free) is an industry-standard identifier for a specific type of vulnerability. Including CWE IDs makes your scanner output compatible with other security tools and reports.

Add argument parsing and directory scanning

Your scanner needs a way to accept file paths from the command line. It also needs to handle scanning an entire directory of C files at once.

  • Below the scan_file() function in vulnscan.py, add the print_findings() function that formats output for the terminal:
def print_findings(findings: list, filepath: str) -> None:
    """Print findings with color-coded severity."""
    if not findings:
        print(f"\n{COLORS['BOLD']}[CLEAN]{COLORS['RESET']} {filepath} - No vulnerabilities found")
        return

    print(f"\n{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}File: {filepath}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}Findings: {len(findings)}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")

    for i, finding in enumerate(findings, 1):
        severity = finding.get("severity", "UNKNOWN")
        color = COLORS.get(severity, COLORS["RESET"])

        print(f"\n  {color}[{severity}]{COLORS['RESET']} Finding #{i}")
        print(f"    Line:     {finding.get('line', 'N/A')}")
        print(f"    CWE:      {finding.get('cwe_id', 'N/A')}")
        print(f"    Category: {finding.get('category', 'N/A')}")
        print(f"    Issue:    {finding.get('description', 'N/A')}")
        print(f"    Exploit:  {finding.get('exploit_scenario', 'N/A')}")
        print(f"    Fix:      {finding.get('fix_recommendation', 'N/A')}")

What does this code do?

This function takes a list of findings and prints them in a readable format. Each finding shows its severity in color (red for CRITICAL, yellow for HIGH, cyan for MEDIUM) along with the line number, CWE ID, description, exploit scenario, and fix recommendation.

If a file has no findings, it prints a "[CLEAN]" message instead.

  • Below print_findings(), add the scan_directory() function, the main() function with argument parsing, and the entry point:
def scan_directory(client: anthropic.Anthropic, dirpath: str) -> list:
    """Scan all .c files in a directory."""
    all_findings = []
    path = Path(dirpath)

    c_files = sorted(path.glob("*.c"))
    if not c_files:
        print(f"No .c files found in {dirpath}", file=sys.stderr)
        return []

    print(f"\n{COLORS['BOLD']}Scanning {len(c_files)} file(s) in {dirpath}...{COLORS['RESET']}")

    for c_file in c_files:
        print(f"\n  Analyzing: {c_file.name}...", end="", flush=True)
        findings = scan_file(client, str(c_file))
        all_findings.extend(findings)
        print(f" {len(findings)} finding(s)")

    return all_findings


def main():
    parser = argparse.ArgumentParser(
        description="Scan C source code for security vulnerabilities using Claude"
    )
    parser.add_argument(
        "path",
        help="Path to a .c file or directory containing .c files",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output raw JSON findings to stdout",
    )
    parser.add_argument(
        "--output",
        "-o",
        type=str,
        help="Write JSON results to a file",
    )

    args = parser.parse_args()

    # Initialize the Anthropic client
    client = anthropic.Anthropic()

    target = Path(args.path)

    if target.is_file():
        findings = scan_file(client, str(target))
        if args.json:
            print(json.dumps(findings, indent=2))
        else:
            print_findings(findings, str(target))
    elif target.is_dir():
        findings = scan_directory(client, str(target))
        if args.json:
            print(json.dumps(findings, indent=2))
        else:
            for filepath in sorted(set(f.get("file", "") for f in findings)):
                file_findings = [f for f in findings if f.get("file") == filepath]
                print_findings(file_findings, filepath)

            # Summary
            print(f"\n{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")
            print(f"{COLORS['BOLD']}SCAN SUMMARY{COLORS['RESET']}")
            print(f"  Total findings: {len(findings)}")
            for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
                count = sum(1 for f in findings if f.get("severity") == sev)
                if count > 0:
                    color = COLORS.get(sev, COLORS["RESET"])
                    print(f"  {color}{sev}: {count}{COLORS['RESET']}")
    else:
        print(f"Error: {args.path} is not a valid file or directory", file=sys.stderr)
        sys.exit(1)

    # Write to output file if specified
    if args.output:
        with open(args.output, "w") as f:
            json.dump(findings, f, indent=2)
        print(f"\nResults written to: {args.output}")


if __name__ == "__main__":
    main()

What does this code do?

scan_directory() finds all .c files in a folder and calls scan_file() on each one. It prints progress as it goes so you can see which file is being analyzed.

The main() function uses argparse to accept a positional path (file or directory) plus two optional flags: --json for raw JSON output and --output to write results to a file. The anthropic.Anthropic() client automatically reads your ANTHROPIC_API_KEY environment variable.

  • Save vulnscan.py.
  • Verify the file has no syntax errors by running:
python3 vulnscan.py --help

You should see a usage message showing the positional path argument and the --json and --output flags.

Seeing ModuleNotFoundError: No module named 'anthropic'?

Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, activate it by running source venv/bin/activate from inside your vulnscan/ folder.

help me fix a ModuleNotFoundError for anthropic in my Python project

Why Claude API instead of a pattern-matching tool?

Traditional static analysis tools like grep-based scanners or Semgrep match known patterns ("is there a strcpy call?"). Your scanner does something fundamentally different. Claude traces data flow through multiple function calls, reasons about whether a pattern is actually exploitable in context, and catches logic bugs that have no simple textual signature.

The tradeoff is that LLM analysis is non-deterministic. The same file may produce slightly different findings across runs. Your structured system prompt minimizes this variance by anchoring Claude to a specific output format and severity scale.

✔️ Awesome, I've got everything!

Make sure you have saved vulnscan.py and that python3 vulnscan.py --help prints the usage message.

ⓧ I'd like to double check the full code

Here is the complete vulnscan.py file:

#!/usr/bin/env python3
"""Linux C Code Vulnerability Scanner powered by Claude."""

import argparse
import json
import os
import sys
from pathlib import Path

import anthropic

SYSTEM_PROMPT = """You are a senior security engineer specializing in Linux C code review. \
Your task is to analyze C source code for security vulnerabilities.

Focus on these vulnerability classes:
- Buffer overflow (stack and heap): strcpy, strcat, sprintf, gets, scanf without bounds, off-by-one errors
- Use-after-free and double-free: dangling pointers, freed memory access
- Format string vulnerabilities: user input as format strings in printf-family functions
- Integer overflow/underflow: allocation size overflows, signed/unsigned confusion, truncation
- TOCTOU race conditions: access() before open(), stat() before fopen(), symlink attacks
- Privilege escalation: unchecked setuid/seteuid return values, improper privilege dropping
- Race conditions: shared state without locks, check-then-act patterns
- Unsafe library calls: gets, mktemp, tmpnam, alloca with unchecked sizes

For each vulnerability found, provide:
- line: the line number where the vulnerability exists
- severity: CRITICAL, HIGH, MEDIUM, or LOW
- cwe_id: the relevant CWE identifier (e.g., CWE-120, CWE-416, CWE-134)
- category: short category name (e.g., buffer_overflow, use_after_free, format_string)
- description: clear explanation of the vulnerability
- exploit_scenario: how an attacker could exploit this
- fix_recommendation: specific code fix or mitigation

Respond with ONLY a JSON array of findings. If no vulnerabilities are found, respond with an empty array [].
Do not include markdown fences, explanations, or any other text outside the JSON array.

Example output format:
[
  {
    "line": 15,
    "severity": "CRITICAL",
    "cwe_id": "CWE-120",
    "category": "buffer_overflow",
    "description": "strcpy copies user input into fixed-size stack buffer without bounds checking",
    "exploit_scenario": "Attacker provides input longer than 64 bytes to overwrite return address and execute arbitrary code",
    "fix_recommendation": "Replace strcpy with strncpy(buffer, user_input, sizeof(buffer) - 1) and null-terminate"
  }
]"""

# ANSI color codes for terminal output
COLORS = {
    "CRITICAL": "\033[91m",  # Red
    "HIGH": "\033[93m",      # Yellow
    "MEDIUM": "\033[96m",    # Cyan
    "LOW": "\033[37m",       # White
    "RESET": "\033[0m",
    "BOLD": "\033[1m",
}


def scan_file(client: anthropic.Anthropic, filepath: str) -> list:
    """Scan a single C file for vulnerabilities using Claude."""
    path = Path(filepath)
    if not path.exists():
        print(f"Error: File not found: {filepath}", file=sys.stderr)
        return []

    code = path.read_text()
    if not code.strip():
        return []

    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=4096,
        system=SYSTEM_PROMPT,
        messages=[
            {
                "role": "user",
                "content": f"Analyze this C source file for security vulnerabilities:\n\nFilename: {path.name}\n\n```c\n{code}\n```",
            }
        ],
    )

    response_text = message.content[0].text.strip()

    # Handle potential markdown fences in response
    if response_text.startswith("```"):
        lines = response_text.split("\n")
        response_text = "\n".join(lines[1:-1])

    try:
        findings = json.loads(response_text)
    except json.JSONDecodeError:
        # Try to extract JSON array from response
        start = response_text.find("[")
        end = response_text.rfind("]") + 1
        if start != -1 and end > start:
            try:
                findings = json.loads(response_text[start:end])
            except json.JSONDecodeError:
                print(f"Warning: Could not parse response for {filepath}", file=sys.stderr)
                return []
        else:
            print(f"Warning: No JSON found in response for {filepath}", file=sys.stderr)
            return []

    # Add filename to each finding
    for finding in findings:
        finding["file"] = str(path.name)

    return findings


def print_findings(findings: list, filepath: str) -> None:
    """Print findings with color-coded severity."""
    if not findings:
        print(f"\n{COLORS['BOLD']}[CLEAN]{COLORS['RESET']} {filepath} - No vulnerabilities found")
        return

    print(f"\n{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}File: {filepath}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}Findings: {len(findings)}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")

    for i, finding in enumerate(findings, 1):
        severity = finding.get("severity", "UNKNOWN")
        color = COLORS.get(severity, COLORS["RESET"])

        print(f"\n  {color}[{severity}]{COLORS['RESET']} Finding #{i}")
        print(f"    Line:     {finding.get('line', 'N/A')}")
        print(f"    CWE:      {finding.get('cwe_id', 'N/A')}")
        print(f"    Category: {finding.get('category', 'N/A')}")
        print(f"    Issue:    {finding.get('description', 'N/A')}")
        print(f"    Exploit:  {finding.get('exploit_scenario', 'N/A')}")
        print(f"    Fix:      {finding.get('fix_recommendation', 'N/A')}")


def scan_directory(client: anthropic.Anthropic, dirpath: str) -> list:
    """Scan all .c files in a directory."""
    all_findings = []
    path = Path(dirpath)

    c_files = sorted(path.glob("*.c"))
    if not c_files:
        print(f"No .c files found in {dirpath}", file=sys.stderr)
        return []

    print(f"\n{COLORS['BOLD']}Scanning {len(c_files)} file(s) in {dirpath}...{COLORS['RESET']}")

    for c_file in c_files:
        print(f"\n  Analyzing: {c_file.name}...", end="", flush=True)
        findings = scan_file(client, str(c_file))
        all_findings.extend(findings)
        print(f" {len(findings)} finding(s)")

    return all_findings


def main():
    parser = argparse.ArgumentParser(
        description="Scan C source code for security vulnerabilities using Claude"
    )
    parser.add_argument(
        "path",
        help="Path to a .c file or directory containing .c files",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output raw JSON findings to stdout",
    )
    parser.add_argument(
        "--output",
        "-o",
        type=str,
        help="Write JSON results to a file",
    )

    args = parser.parse_args()

    # Initialize the Anthropic client
    client = anthropic.Anthropic()

    target = Path(args.path)

    if target.is_file():
        findings = scan_file(client, str(target))
        if args.json:
            print(json.dumps(findings, indent=2))
        else:
            print_findings(findings, str(target))
    elif target.is_dir():
        findings = scan_directory(client, str(target))
        if args.json:
            print(json.dumps(findings, indent=2))
        else:
            for filepath in sorted(set(f.get("file", "") for f in findings)):
                file_findings = [f for f in findings if f.get("file") == filepath]
                print_findings(file_findings, filepath)

            # Summary
            print(f"\n{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")
            print(f"{COLORS['BOLD']}SCAN SUMMARY{COLORS['RESET']}")
            print(f"  Total findings: {len(findings)}")
            for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
                count = sum(1 for f in findings if f.get("severity") == sev)
                if count > 0:
                    color = COLORS.get(sev, COLORS["RESET"])
                    print(f"  {color}{sev}: {count}{COLORS['RESET']}")
    else:
        print(f"Error: {args.path} is not a valid file or directory", file=sys.stderr)
        sys.exit(1)

    # Write to output file if specified
    if args.output:
        with open(args.output, "w") as f:
            json.dump(findings, f, indent=2)
        print(f"\nResults written to: {args.output}")


if __name__ == "__main__":
    main()

Your scanner is fully built. Next up, you will run it against your vulnerable C test files and see what Claude finds.

Parse and Display Results

Your scanner can send C code to Claude and get a response back. The next challenge is turning that response into something useful.

LLM responses are not always perfectly formatted. Claude might wrap its JSON in markdown code fences, or include explanatory text alongside the data. You need resilient parsing that handles these edge cases gracefully, plus a display layer that makes vulnerability findings easy to read at a glance.

In this step, get ready to:
  • Add resilient JSON parsing to handle unexpected response formats.
  • Build color-coded terminal output for vulnerability findings.
  • Add output flags for JSON export and file writing.
Add resilient JSON parsing to scan_file()

Claude's system prompt asks for a raw JSON array, but LLMs occasionally wrap output in markdown fences or add commentary. Your parser needs to handle these variations without crashing.

  • In vulnscan.py, find the scan_file() function.
  • Locate this line near the end of the function:
    response_text = message.content[0].text.strip()
  • Add the following response parsing logic directly below that line:
    # Handle potential markdown fences in response
    if response_text.startswith("```"):
        lines = response_text.split("\n")
        response_text = "\n".join(lines[1:-1])

    try:
        findings = json.loads(response_text)
    except json.JSONDecodeError:
        # Try to extract JSON array from response
        start = response_text.find("[")
        end = response_text.rfind("]") + 1
        if start != -1 and end > start:
            try:
                findings = json.loads(response_text[start:end])
            except json.JSONDecodeError:
                print(f"Warning: Could not parse response for {filepath}", file=sys.stderr)
                return []
        else:
            print(f"Warning: No JSON found in response for {filepath}", file=sys.stderr)
            return []

    # Add filename to each finding
    for finding in findings:
        finding["file"] = str(path.name)

    return findings

What does this code do?

This parsing logic has three layers of defense against unexpected formatting:

  • If the response starts with triple backticks, it strips the first and last lines (the markdown fence markers) to extract just the JSON content.
  • It tries to parse the entire response as JSON first. If that fails, it searches for the first [ and last ] to extract a JSON array from surrounding text.
  • If all parsing attempts fail, it prints a warning to stderr and returns an empty list instead of crashing.
  • Save vulnscan.py.

You won't see a change yet. The next chunk builds the display function that renders these parsed findings.

Seeing indentation errors?

All of this code lives inside the scan_file() function, so every line needs at least one level of indentation (4 spaces). Make sure the if response_text.startswith line is at the same indent level as the response_text = message.content[0].text.strip() line above it.

Help me fix an indentation error in my scan_file() function.

Build the print_findings() function

Raw JSON findings are useful for machines, but humans need something scannable. Color-coded severity levels let you instantly spot the critical issues in a wall of output.

  • In vulnscan.py, add the following function between scan_file() and scan_directory():
def print_findings(findings: list, filepath: str) -> None:
    """Print findings with color-coded severity."""
    if not findings:
        print(f"\n{COLORS['BOLD']}[CLEAN]{COLORS['RESET']} {filepath} - No vulnerabilities found")
        return

    print(f"\n{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}File: {filepath}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}Findings: {len(findings)}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")

    for i, finding in enumerate(findings, 1):
        severity = finding.get("severity", "UNKNOWN")
        color = COLORS.get(severity, COLORS["RESET"])

        print(f"\n  {color}[{severity}]{COLORS['RESET']} Finding #{i}")
        print(f"    Line:     {finding.get('line', 'N/A')}")
        print(f"    CWE:      {finding.get('cwe_id', 'N/A')}")
        print(f"    Category: {finding.get('category', 'N/A')}")
        print(f"    Issue:    {finding.get('description', 'N/A')}")
        print(f"    Exploit:  {finding.get('exploit_scenario', 'N/A')}")
        print(f"    Fix:      {finding.get('fix_recommendation', 'N/A')}")

What does this code do?

This function handles two display paths based on whether any vulnerabilities were found:

  • If the findings list is empty, it prints a bold [CLEAN] tag with the filename. This gives clear confirmation that a file passed analysis.
  • For files with findings, it prints a header with the filename and count, then iterates each finding. The severity label gets color-coded using the COLORS dictionary you defined earlier (red for CRITICAL, yellow for HIGH, cyan for MEDIUM, white for LOW).
  • Each finding displays its line number, CWE ID, category, description, exploit scenario, and fix recommendation in an indented layout.

What are ANSI escape codes?

The \033[91m sequences in the COLORS dictionary are ANSI escape codes. Terminals interpret these special character sequences as formatting instructions. \033[91m means "start printing in red" and \033[0m means "reset to default." They work natively in macOS Terminal and iTerm2.

  • Save vulnscan.py.

You won't see output yet because we still need to wire the display into the main function's output handling.

Function placement causing errors?

Make sure print_findings() is defined BEFORE scan_directory() in the file. Python reads top to bottom, so any function must be defined above the code that calls it.

Help me fix a NameError for print_findings in my scanner.

Add output flags to main()

Your scanner already accepts --json and --output arguments from argparse. Now you need the logic that actually uses them to control how results are displayed and saved.

  • In vulnscan.py, find the main() function. Locate this line:
    target = Path(args.path)
  • Replace everything below that line (to the end of the main() function) with the following output handling logic:
    if target.is_file():
        findings = scan_file(client, str(target))
        if args.json:
            print(json.dumps(findings, indent=2))
        else:
            print_findings(findings, str(target))
    elif target.is_dir():
        findings = scan_directory(client, str(target))
        if args.json:
            print(json.dumps(findings, indent=2))
        else:
            for filepath in sorted(set(f.get("file", "") for f in findings)):
                file_findings = [f for f in findings if f.get("file") == filepath]
                print_findings(file_findings, filepath)

            # Summary
            print(f"\n{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")
            print(f"{COLORS['BOLD']}SCAN SUMMARY{COLORS['RESET']}")
            print(f"  Total findings: {len(findings)}")
            for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
                count = sum(1 for f in findings if f.get("severity") == sev)
                if count > 0:
                    color = COLORS.get(sev, COLORS["RESET"])
                    print(f"  {color}{sev}: {count}{COLORS['RESET']}")
    else:
        print(f"Error: {args.path} is not a valid file or directory", file=sys.stderr)
        sys.exit(1)

    # Write to output file if specified
    if args.output:
        with open(args.output, "w") as f:
            json.dump(findings, f, indent=2)
        print(f"\nResults written to: {args.output}")

What does this code do?

This block handles three output modes and two target types:

  • When --json is passed, it dumps raw JSON to stdout. This makes your scanner composable with other tools (pipe it into jq, feed it to a dashboard, or process it in a CI pipeline).
  • For directory scans without --json, it groups findings by file, prints each file's results, then shows a summary with per-severity counts.
  • The --output flag writes findings to a file regardless of which display mode is active. This lets you save results for later while still viewing them in the terminal.
  • Save vulnscan.py.
  • Verify your script runs without import errors by running:
python3 vulnscan.py --help

You should see a usage message showing the path argument and the --json and --output flags.

Seeing a SyntaxError or ImportError?

Check that your virtual environment is still active. You should see (venv) at the start of your terminal prompt. If not, activate it again with source venv/bin/activate.

If you see a SyntaxError, check for mismatched f-string quotes or missing colons. The f-strings in this code use single quotes inside double-quoted strings.

Help me debug a SyntaxError in my vulnscan.py main() function.

✔️ Awesome, I've got everything!

Great. Double check you have saved vulnscan.py and your virtual environment is active.

ⓧ I'd like to double check the full code

Here is the complete vulnscan.py file as it should look at the end of this step:

#!/usr/bin/env python3
"""Linux C Code Vulnerability Scanner powered by Claude."""

import argparse
import json
import os
import sys
from pathlib import Path

import anthropic

SYSTEM_PROMPT = """You are a senior security engineer specializing in Linux C code review. \
Your task is to analyze C source code for security vulnerabilities.

Focus on these vulnerability classes:
- Buffer overflow (stack and heap): strcpy, strcat, sprintf, gets, scanf without bounds, off-by-one errors
- Use-after-free and double-free: dangling pointers, freed memory access
- Format string vulnerabilities: user input as format strings in printf-family functions
- Integer overflow/underflow: allocation size overflows, signed/unsigned confusion, truncation
- TOCTOU race conditions: access() before open(), stat() before fopen(), symlink attacks
- Privilege escalation: unchecked setuid/seteuid return values, improper privilege dropping
- Race conditions: shared state without locks, check-then-act patterns
- Unsafe library calls: gets, mktemp, tmpnam, alloca with unchecked sizes

For each vulnerability found, provide:
- line: the line number where the vulnerability exists
- severity: CRITICAL, HIGH, MEDIUM, or LOW
- cwe_id: the relevant CWE identifier (e.g., CWE-120, CWE-416, CWE-134)
- category: short category name (e.g., buffer_overflow, use_after_free, format_string)
- description: clear explanation of the vulnerability
- exploit_scenario: how an attacker could exploit this
- fix_recommendation: specific code fix or mitigation

Respond with ONLY a JSON array of findings. If no vulnerabilities are found, respond with an empty array [].
Do not include markdown fences, explanations, or any other text outside the JSON array.

Example output format:
[
  {
    "line": 15,
    "severity": "CRITICAL",
    "cwe_id": "CWE-120",
    "category": "buffer_overflow",
    "description": "strcpy copies user input into fixed-size stack buffer without bounds checking",
    "exploit_scenario": "Attacker provides input longer than 64 bytes to overwrite return address and execute arbitrary code",
    "fix_recommendation": "Replace strcpy with strncpy(buffer, user_input, sizeof(buffer) - 1) and null-terminate"
  }
]"""

# ANSI color codes for terminal output
COLORS = {
    "CRITICAL": "\033[91m",  # Red
    "HIGH": "\033[93m",      # Yellow
    "MEDIUM": "\033[96m",    # Cyan
    "LOW": "\033[37m",       # White
    "RESET": "\033[0m",
    "BOLD": "\033[1m",
}


def scan_file(client: anthropic.Anthropic, filepath: str) -> list:
    """Scan a single C file for vulnerabilities using Claude."""
    path = Path(filepath)
    if not path.exists():
        print(f"Error: File not found: {filepath}", file=sys.stderr)
        return []

    code = path.read_text()
    if not code.strip():
        return []

    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=4096,
        system=SYSTEM_PROMPT,
        messages=[
            {
                "role": "user",
                "content": f"Analyze this C source file for security vulnerabilities:\n\nFilename: {path.name}\n\n```c\n{code}\n```",
            }
        ],
    )

    response_text = message.content[0].text.strip()

    # Handle potential markdown fences in response
    if response_text.startswith("```"):
        lines = response_text.split("\n")
        response_text = "\n".join(lines[1:-1])

    try:
        findings = json.loads(response_text)
    except json.JSONDecodeError:
        # Try to extract JSON array from response
        start = response_text.find("[")
        end = response_text.rfind("]") + 1
        if start != -1 and end > start:
            try:
                findings = json.loads(response_text[start:end])
            except json.JSONDecodeError:
                print(f"Warning: Could not parse response for {filepath}", file=sys.stderr)
                return []
        else:
            print(f"Warning: No JSON found in response for {filepath}", file=sys.stderr)
            return []

    # Add filename to each finding
    for finding in findings:
        finding["file"] = str(path.name)

    return findings


def print_findings(findings: list, filepath: str) -> None:
    """Print findings with color-coded severity."""
    if not findings:
        print(f"\n{COLORS['BOLD']}[CLEAN]{COLORS['RESET']} {filepath} - No vulnerabilities found")
        return

    print(f"\n{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}File: {filepath}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}Findings: {len(findings)}{COLORS['RESET']}")
    print(f"{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")

    for i, finding in enumerate(findings, 1):
        severity = finding.get("severity", "UNKNOWN")
        color = COLORS.get(severity, COLORS["RESET"])

        print(f"\n  {color}[{severity}]{COLORS['RESET']} Finding #{i}")
        print(f"    Line:     {finding.get('line', 'N/A')}")
        print(f"    CWE:      {finding.get('cwe_id', 'N/A')}")
        print(f"    Category: {finding.get('category', 'N/A')}")
        print(f"    Issue:    {finding.get('description', 'N/A')}")
        print(f"    Exploit:  {finding.get('exploit_scenario', 'N/A')}")
        print(f"    Fix:      {finding.get('fix_recommendation', 'N/A')}")


def scan_directory(client: anthropic.Anthropic, dirpath: str) -> list:
    """Scan all .c files in a directory."""
    all_findings = []
    path = Path(dirpath)

    c_files = sorted(path.glob("*.c"))
    if not c_files:
        print(f"No .c files found in {dirpath}", file=sys.stderr)
        return []

    print(f"\n{COLORS['BOLD']}Scanning {len(c_files)} file(s) in {dirpath}...{COLORS['RESET']}")

    for c_file in c_files:
        print(f"\n  Analyzing: {c_file.name}...", end="", flush=True)
        findings = scan_file(client, str(c_file))
        all_findings.extend(findings)
        print(f" {len(findings)} finding(s)")

    return all_findings


def main():
    parser = argparse.ArgumentParser(
        description="Scan C source code for security vulnerabilities using Claude"
    )
    parser.add_argument(
        "path",
        help="Path to a .c file or directory containing .c files",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output raw JSON findings to stdout",
    )
    parser.add_argument(
        "--output",
        "-o",
        type=str,
        help="Write JSON results to a file",
    )

    args = parser.parse_args()

    # Initialize the Anthropic client
    client = anthropic.Anthropic()

    target = Path(args.path)

    if target.is_file():
        findings = scan_file(client, str(target))
        if args.json:
            print(json.dumps(findings, indent=2))
        else:
            print_findings(findings, str(target))
    elif target.is_dir():
        findings = scan_directory(client, str(target))
        if args.json:
            print(json.dumps(findings, indent=2))
        else:
            for filepath in sorted(set(f.get("file", "") for f in findings)):
                file_findings = [f for f in findings if f.get("file") == filepath]
                print_findings(file_findings, filepath)

            # Summary
            print(f"\n{COLORS['BOLD']}{'=' * 60}{COLORS['RESET']}")
            print(f"{COLORS['BOLD']}SCAN SUMMARY{COLORS['RESET']}")
            print(f"  Total findings: {len(findings)}")
            for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
                count = sum(1 for f in findings if f.get("severity") == sev)
                if count > 0:
                    color = COLORS.get(sev, COLORS["RESET"])
                    print(f"  {color}{sev}: {count}{COLORS['RESET']}")
    else:
        print(f"Error: {args.path} is not a valid file or directory", file=sys.stderr)
        sys.exit(1)

    # Write to output file if specified
    if args.output:
        with open(args.output, "w") as f:
            json.dump(findings, f, indent=2)
        print(f"\nResults written to: {args.output}")


if __name__ == "__main__":
    main()

Your scanner now handles messy LLM output gracefully and presents findings in a readable, color-coded format. Next up, you will run it against your vulnerable C files and see what Claude catches.

Test Your Scanner

Your scanner is built. Your vulnerable C files are ready. Now it is time to put them together and see if Claude can actually find the security bugs you planted.

In this step, you will run your scanner against each sample file individually, batch-scan the entire directory, and test edge cases to make sure the tool handles clean and empty files gracefully.

In this step, get ready to:
  • Scan individual vulnerable C files and verify Claude identifies the planted bugs.
  • Run a batch scan of the entire samples directory.
  • Test edge cases with a safe file and an empty file.
Scan individual vulnerable files

Each sample file contains a specific class of vulnerability. Running the scanner against them one at a time lets you verify it correctly identifies each bug type with the right severity and CWE ID.

  • Make sure your virtual environment is active (you should see (venv) in your terminal prompt).
  • Run the scanner against buffer_overflow.c by running this command:
python3 vulnscan.py samples/buffer_overflow.c

You should see color-coded findings in your terminal. Look for a CRITICAL severity finding related to the strcpy buffer overflow (CWE-120) and an off-by-one error.

Why might my results look slightly different?

Claude is not deterministic. Each scan may produce slightly different descriptions, line numbers, or even find additional issues you did not expect. The key things to check are that it identifies the correct vulnerability class (buffer overflow), assigns a high severity, and references the right CWE ID.

  • Now scan use_after_free.c by running:
python3 vulnscan.py samples/use_after_free.c

You should see a CRITICAL finding for use-after-free (CWE-416). Claude should identify that the admin pointer is accessed after being freed.

  • Scan the remaining three files by running each command:
python3 vulnscan.py samples/format_string.c
python3 vulnscan.py samples/integer_overflow.c
python3 vulnscan.py samples/toctou.c

Here is what to look for in each scan:

  • format_string.c: HIGH or CRITICAL format string vulnerabilities (CWE-134) where user input is used as a format string in printf and snprintf.
  • integer_overflow.c: HIGH integer overflow findings (CWE-190) for the multiplication overflow in safe_alloc() and a signed/unsigned comparison bug in copy_with_offset().
  • toctou.c: MEDIUM or HIGH TOCTOU race condition findings (CWE-367) for the stat() before fopen() pattern and the access() before fopen() pattern.

Seeing a connection or authentication error?

Make sure your ANTHROPIC_API_KEY environment variable is set in your current terminal session. Run echo $ANTHROPIC_API_KEY to check. If it is empty, run source ~/.zshrc to reload your environment.

Also verify your Console account has credits available at console.anthropic.com.

My scanner is returning an authentication or API error

Run a batch scan on the samples directory

Scanning files one at a time works, but in practice you want to point the tool at an entire directory. Your scanner already supports this through the scan_directory() function.

  • Scan all C files in the samples/ directory by running:
python3 vulnscan.py samples/

This takes a bit longer since Claude analyzes each file sequentially. You should see a progress indicator as each file is analyzed, followed by grouped findings and a scan summary at the end.

What should the summary look like?

At the bottom of the output, you will see a SCAN SUMMARY with the total number of findings and a breakdown by severity (CRITICAL, HIGH, MEDIUM, LOW). Expect somewhere between 10 and 20 total findings across all files, though the exact number varies with each run.

The hello_safe.c file should show up as [CLEAN] with zero findings. That confirms your scanner does not produce false positives on trivially safe code.

  • Test the JSON output flag by running:
python3 vulnscan.py samples/buffer_overflow.c --json

You should see raw JSON printed to your terminal instead of the color-coded report. This format is useful for piping results into other tools or CI systems.

  • Test the file output flag by writing results to a JSON file:
python3 vulnscan.py samples/ --output results.json
  • Verify the file was created by running:
cat results.json | python3 -m json.tool | head -30

You should see the first 30 lines of a nicely formatted JSON array containing all the vulnerability findings from the batch scan.

Seeing 'No .c files found' or zero findings?

Make sure you are running the command from inside the vulnscan/ directory (not from inside samples/ itself). Run ls samples/ to confirm your C files are there.

My batch scan is not finding any files or returning zero results

Test edge cases

A robust tool handles unexpected input gracefully. You already have hello_safe.c in your samples directory, which is a perfectly safe file with no vulnerabilities. Now you will also test what happens with an empty file.

  • Scan the safe file by running:
python3 vulnscan.py samples/hello_safe.c

You should see a [CLEAN] message indicating no vulnerabilities were found. Claude correctly recognizes that a simple hello world program has no security issues.

  • Create an empty C file to test the other edge case:
touch samples/empty.c
  • Run the scanner against the empty file:
python3 vulnscan.py samples/empty.c

The scanner should produce no output and no errors. Your scan_file() function detects empty files and returns an empty list before sending anything to the API. This saves you API credits on files with nothing to analyze.

  • Clean up the empty test file since it is not part of your real sample set:
rm samples/empty.c

What makes this scanner better than grep?

Traditional pattern-matching tools like grep can find calls to strcpy, but they cannot reason about data flow. Your Claude-powered scanner traces user input through multiple function calls, understands context, and identifies logic vulnerabilities like TOCTOU race conditions that have no single-line signature.

It also provides exploit scenarios and fix recommendations. No regex can do that.

Getting a JSON parse warning?

If you see a "Could not parse response" warning, Claude may have included extra text around the JSON. Your code already handles this with a fallback that extracts the JSON array from the response. If it happens consistently for one file, try running the scan again. LLM outputs can vary between calls.

My scanner keeps failing to parse Claude's response

Your scanner is fully tested and working. You have confirmed it catches real vulnerabilities across five different bug classes, handles batch scanning, outputs structured JSON, and gracefully handles edge cases. Next up, you can take on the Secret Mission to add a fix mode that generates patched code.

Secret mission

Add Fix Mode

Your scanner finds vulnerabilities, but what if it could also fix them? Add a --fix flag that generates patched C code with every vulnerability remediated and explained.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs when your scanner is not running. The Anthropic API is pay-as-you-go, meaning you only pay when you actively send requests. Decide whether to keep your resources ready, pause them, or delete them entirely.

Resources you used:

  • Anthropic Console account and API key.
  • Python virtual environment (venv folder inside vulnscan).
  • Project directory (vulnscan/) with scanner script and sample C files.
  • Environment variable ANTHROPIC_API_KEY in ~/.zshrc.

Keep everything running

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

  • Your project folder, virtual environment, and API key all stay in place.
  • You can reactivate your virtual environment anytime with source venv/bin/activate from inside the vulnscan folder.
  • No charges accrue unless you actively run the scanner.

Pause - I'll come back to this later

Deactivate your virtual environment to free up terminal state. Your files remain on disk for later.

  • Run deactivate in your terminal to exit the virtual environment.
  • Your vulnscan/ folder and API key stay intact. When you return, just cd into the folder and reactivate.

Delete - I don't want to use this again

Remove all project files and clean up your environment variable.

  • Delete the project folder by running rm -rf ~/Desktop/vulnscan in your terminal.
  • Open ~/.zshrc in a text editor and remove the line that exports ANTHROPIC_API_KEY.
  • Optionally, revoke your API key in the Anthropic Console under Settings to prevent any future usage.

Nice Work!

Nice Work!

You just built an AI-powered vulnerability scanner that performs semantic security analysis on Linux C code. This is the kind of tool that security teams actually use in production workflows.

You've learned how to:

  • Built a Python CLI tool that leverages the Claude API to perform deep semantic code analysis on C source files.
  • Designed a security-focused system prompt targeting Linux C vulnerability classes including buffer overflow, use-after-free, format string, integer overflow, and TOCTOU.
  • Produced structured JSON vulnerability reports with CWE IDs, severity ratings, exploit scenarios, and fix recommendations.
  • Added a fix mode that generates patched C code with vulnerabilities remediated.

Ready to quiz yourself?