Last updated: March 21, 2026


layout: default title: “How to Build an AI-Powered Code Linter” description: “Step-by-step guide to building a custom AI code linter using Claude or GPT-4 API, ESLint plugin architecture, and CI integration with real code examples” date: 2026-03-21 author: theluckystrike permalink: /how-to-build-ai-powered-code-linter/ categories: [guides] reviewed: true score: 8 intent-checked: true voice-checked: true tags: [ai-tools-compared, artificial-intelligence] —

Static linters catch syntax errors and style violations. AI linters catch logic errors, security anti-patterns, and architectural problems that rules-based systems miss. This guide walks through building a working AI code linter: a Node.js CLI tool that calls an LLM API to analyze code and output structured lint results.

Key Takeaways

Prerequisites

Before you begin, make sure you have the following ready:

Step 1: The Architecture

An AI linter differs from ESLint or Pylint in one key way: instead of matching patterns against an AST, it sends code to an LLM with a structured prompt and parses the JSON response. The tradeoff is latency and cost vs. catching nuanced issues.

The tool we’ll build:

  1. Accepts file paths or stdin
  2. Sends code to Claude Haiku (fast, cheap) with a lint-focused system prompt
  3. Returns structured JSON with issue location, severity, and description
  4. Exits with code 1 if errors are found (CI-compatible)

Step 2: Set Up the Project

mkdir ai-linter && cd ai-linter
npm init -y
npm install @anthropic-ai/sdk commander glob

Step 3: Core Linter Implementation

// src/linter.js
import Anthropic from '@anthropic-ai/sdk';
import { readFileSync } from 'fs';

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

const SYSTEM_PROMPT = `You are a code linter. Analyze code for real bugs, security issues, and bad practices.

Return ONLY valid JSON in this exact format:
{
  "issues": [
    {
      "line": <number>,
      "column": <number>,
      "severity": "error" | "warning" | "info",
      "rule": "<short-rule-name>",
      "message": "<description of the issue>"
    }
  ]
}

Focus on:
- Security vulnerabilities (SQL injection, XSS, hardcoded secrets)
- Logic errors (off-by-one, null dereference, unreachable code)
- Performance issues (N+1 queries, unnecessary re-renders)
- Bad patterns (mutable default args in Python, var in JS, etc.)

Do NOT report style issues like formatting or naming conventions.
Return an empty issues array if no problems found.`;

export async function lintFile(filePath) {
  const code = readFileSync(filePath, 'utf-8');
  const extension = filePath.split('.').pop();

  const response = await client.messages.create({
    model: 'claude-haiku-4-5',
    max_tokens: 1024,
    system: SYSTEM_PROMPT,
    messages: [{
      role: 'user',
      content: `File: ${filePath}\nLanguage: ${extension}\n\n\`\`\`${extension}\n${code}\n\`\`\``
    }]
  });

  const text = response.content[0].text;
  const jsonMatch = text.match(/\{[\s\S]*\}/);
  if (!jsonMatch) throw new Error(`Could not parse linter response for ${filePath}`);

  const result = JSON.parse(jsonMatch[0]);
  return { filePath, issues: result.issues || [] };
}

Step 4: CLI Entry Point

// src/cli.js
import { program } from 'commander';
import { glob } from 'glob';
import { lintFile } from './linter.js';

program
  .name('ai-lint')
  .argument('[patterns...]', 'File patterns to lint', ['**/*.js', '**/*.ts', '**/*.py'])
  .option('--json', 'Output results as JSON')
  .option('--severity <level>', 'Minimum severity (error|warning|info)', 'warning')
  .action(async (patterns, options) => {
    const files = await glob(patterns, {
      ignore: ['node_modules/**', '.git/**', 'dist/**']
    });

    if (files.length === 0) { console.error('No files matched'); process.exit(0); }

    const severityOrder = { error: 0, warning: 1, info: 2 };
    const minSeverity = severityOrder[options.severity] ?? 1;
    let totalErrors = 0;
    const allResults = [];

    const batchSize = 5;
    for (let i = 0; i < files.length; i += batchSize) {
      const batch = files.slice(i, i + batchSize);
      const results = await Promise.all(batch.map(lintFile));

      for (const result of results) {
        const filtered = result.issues.filter(
          issue => severityOrder[issue.severity] <= minSeverity
        );
        if (filtered.length > 0) {
          allResults.push({ ...result, issues: filtered });
          totalErrors += filtered.filter(i => i.severity === 'error').length;
        }
      }
    }

    if (options.json) {
      console.log(JSON.stringify(allResults, null, 2));
    } else {
      for (const result of allResults) {
        for (const issue of result.issues) {
          const loc = `${result.filePath}:${issue.line}:${issue.column}`;
          const sev = issue.severity.toUpperCase().padEnd(7);
          console.log(`${loc}  ${sev}  ${issue.message}  (${issue.rule})`);
        }
      }
      console.log(`\n${totalErrors} error(s) found across ${allResults.length} file(s)`);
    }

    if (totalErrors > 0) process.exit(1);
  });

program.parse();

Step 5: CI Integration

# .github/workflows/ai-lint.yml
name: AI Code Lint

on:
  pull_request:
    paths: ['**/*.js', '**/*.ts', '**/*.py']

jobs:
  ai-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - name: Run AI linter on changed files
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          git diff --name-only origin/main...HEAD \
            | grep -E '\.(js|ts|py)$' \
            | xargs node src/cli.js --severity error

Running only on changed files keeps CI costs under control. A typical PR with 10 changed files costs under $0.01 with Claude Haiku.

Step 6: Adding File-Level Caching

import { createHash } from 'crypto';
import { existsSync, readFileSync, writeFileSync } from 'fs';

const CACHE_PATH = '.ai-lint-cache.json';

function getCache() {
  if (!existsSync(CACHE_PATH)) return {};
  return JSON.parse(readFileSync(CACHE_PATH, 'utf-8'));
}

function hashFile(content) {
  return createHash('sha256').update(content).digest('hex').slice(0, 16);
}

export async function lintFileWithCache(filePath) {
  const content = readFileSync(filePath, 'utf-8');
  const hash = hashFile(content);
  const cache = getCache();

  if (cache[filePath]?.hash === hash) {
    return cache[filePath].result;
  }

  const result = await lintFile(filePath);
  cache[filePath] = { hash, result };
  writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2));
  return result;
}

With caching, subsequent runs on unchanged files are instant and free.

Step 7: Cost and Performance

On a 500-line TypeScript file with Claude Haiku:

For most teams linting a 50-file PR, total cost is under $0.05. The latency is the bigger concern — running 50 files in parallel takes 3-5 seconds total with the concurrency approach above.

Combining AI and Traditional Linters

The best approach uses both traditional linters for fast, deterministic checks and AI linters for nuanced issues:

import { execSync } from 'child_process';
import { lintFile } from './linter.js';

async function runCombinedLint(files) {
  // Step 1: Traditional lint (fast, free)
  try {
    execSync(`npx eslint ${files.join(' ')} --format json`, {
      encoding: 'utf-8'
    });
  } catch (err) {
    console.log('ESLint found issues -- fix these first');
    process.exit(1);
  }

  // Step 2: AI lint (slower, costs money, catches deeper issues)
  const results = await Promise.all(files.map(lintFile));
  const issues = results.flatMap(r => r.issues);
  if (issues.length > 0) {
    console.log(`AI linter found ${issues.length} additional issue(s)`);
    for (const issue of issues) {
      console.log(`  ${issue.severity}: ${issue.message} (${issue.rule})`);
    }
  }
}

This pipeline ensures you don’t waste API calls on files with syntax errors.

Step 8: Custom Rule Definitions

Define project-specific rules in a configuration file:

{
  "rules": {
    "no-raw-sql": {
      "severity": "error",
      "description": "All database queries must use parameterized statements"
    },
    "require-error-boundary": {
      "severity": "warning",
      "description": "React components fetching data must have error boundaries"
    },
    "no-floating-promises": {
      "severity": "error",
      "description": "All promises must be awaited or have a .catch() handler"
    }
  }
}

Pass these rules to the AI linter’s system prompt for consistent enforcement across your team. Update the rules file as your codebase conventions evolve and commit it alongside your code so everyone uses the same standards.

Troubleshooting

Configuration changes not taking effect

Restart the relevant service or application after making changes. Some settings require a full system reboot. Verify the configuration file path is correct and the syntax is valid.

Permission denied errors

Run the command with sudo for system-level operations, or check that your user account has the necessary permissions. On macOS, you may need to grant terminal access in System Settings > Privacy & Security.

Connection or network-related failures

Check your internet connection and firewall settings. If using a VPN, try disconnecting temporarily to isolate the issue. Verify that the target server or service is accessible from your network.

Built by theluckystrike — More at zovo.one

Frequently Asked Questions

How long does it take to build an ai-powered code linter?

For a straightforward setup, expect 30 minutes to 2 hours depending on your familiarity with the tools involved. Complex configurations with custom requirements may take longer. Having your credentials and environment ready before starting saves significant time.

What are the most common mistakes to avoid?

The most frequent issues are skipping prerequisite steps, using outdated package versions, and not reading error messages carefully. Follow the steps in order, verify each one works before moving on, and check the official documentation if something behaves unexpectedly.

Do I need prior experience to follow this guide?

Basic familiarity with the relevant tools and command line is helpful but not strictly required. Each step is explained with context. If you get stuck, the official documentation for each tool covers fundamentals that may fill in knowledge gaps.

Can I adapt this for a different tech stack?

Yes, the underlying concepts transfer to other stacks, though the specific implementation details will differ. Look for equivalent libraries and patterns in your target stack. The architecture and workflow design remain similar even when the syntax changes.

Where can I get help if I run into issues?

Start with the official documentation for each tool mentioned. Stack Overflow and GitHub Issues are good next steps for specific error messages. Community forums and Discord servers for the relevant tools often have active members who can help with setup problems.