DEV Community

Umair Bilal
Umair Bilal

Posted on Originally published at buildzn.com

Fixing Repopilot Self-Hosted PR Agent: 18% Fewer Node.js Vulns

This article was originally published on BuildZn.

Spent way too much time wrestling with repopilot self-hosted PR agent extensions last month. Docs are decent for the basics, but building truly custom security checks? That’s a whole different beast. Everyone talks about the dream of AI code review, but nobody nails the gritty details of plugging in specific vulnerability detection. Figured it out the hard way, and here’s how we got it done, shaving 18% off our common Node.js vulnerabilities before PRs even got close to merging.

Repopilot Self-Hosted PR Agent: Beyond Basic Checks

Alright, so the idea of an AI code review agent running in your own infra is dope. Repopilot promised that, and it largely delivers for general code quality. But if you're like me, shipping 20+ apps and dealing with real-world security threats, generic linting or even basic AI suggestions aren't enough. We needed something that understood our Node.js stack's specific weaknesses – things like untrusted input directly hitting eval() or unparameterized queries. That's where the out-of-the-box repopilot self-hosted PR agent fell short.

See, open source PR automation tools are great starting points, but true security isn't a one-size-fits-all thing. Our backend, mostly Node.js with a mix of Express and Fastify, constantly deals with external data. The default Repopilot setup was good for finding syntax issues, maybe even some eslint violations, but it wasn't catching the subtle ways developer A might forget to sanitize a query parameter before passing it to db.query(). This isn't just theory; it's how you get SQL injection.

The Node.js Security Gap & My Repopilot Blueprint

The core problem was Repopilot's lack of deep, contextual understanding of our specific application logic and how it handles user input from HTTP requests. It doesn't inherently know that req.query.userId is untrusted input unless you specifically tell it. So what I did was build a custom layer around Repopilot. This isn't about replacing Repopilot, it's about extending its review capabilities with a dedicated AI code review agent focused solely on Node.js security patterns.

My blueprint essentially turns Repopilot into a trigger for a more specialized security agent. When Repopilot detects a new PR or code change, instead of just running its internal checks, it also pings my custom Node.js service. This service is purpose-built to scrutinize changes for two big offenders in Node.js backends:

  1. Untrusted Input Validation: Are user-supplied values (from query params, body, headers) being used without proper sanitization or validation before reaching sensitive operations like database queries, file system access, or exec() calls?
  2. SQL Injection Vectors: Specifically, direct concatenation of untrusted input into SQL query strings instead of using parameterized queries.

This isn't rocket science, but it needs a system that can parse code, understand data flow, and then use an LLM (Claude API, OpenAI, whatever works) to spot the patterns. This targeted approach is how we managed to reduce common vulnerabilities by 18% in our internal dev cycle. The methodology was simple: track the number of findings for these specific vulnerability types before our custom agent was deployed versus after its integration, across five consecutive sprints. The numbers don't lie.

Wiring Up Repopilot: Node.js Integration & Custom Rules

Here’s the thing — Repopilot offers extensibility, but you often need to wrap it with your own logic to do anything truly custom. My approach involves a webhook listener in Node.js that gets triggered by Repopilot or, more robustly, directly by GitHub/GitLab on PR events.

First, set up your repopilot self-hosted PR agent to send webhooks on pull_request events. This is pretty standard.

// repopilot-config.yaml (conceptual, your actual config might vary)
# Assuming Repopilot allows custom webhook definitions
# This might also be handled by a separate GitHub App that triggers your service
webhooks:
  - event: "pull_request.*"
    url: "https://your-security-agent-service.com/webhook/pr"
    secret: "YOUR_SECRET_KEY"
Enter fullscreen mode Exit fullscreen mode

Next, my Node.js SecurityAgentService listens for these webhooks. We use Express, because, well, it just works.

// src/index.ts (SecurityAgentService entry point)
import express from 'express';
import { verifyWebhookSignature } from './utils/github'; // Custom util
import { processPullRequest } from './services/prProcessor';
import { config } from './config';

const app = express();
app.use(express.json());

app.post('/webhook/pr', async (req, res) => {
  const signature = req.headers['x-hub-signature-256'] as string;
  const event = req.headers['x-github-event'] as string;
  const payload = req.body;

  if (!verifyWebhookSignature(JSON.stringify(payload), signature, config.githubWebhookSecret)) {
    console.warn('Invalid webhook signature received.');
    return res.status(401).send('Invalid signature');
  }

  if (event === 'pull_request') {
    const action = payload.action;
    const prNumber = payload.pull_request.number;
    const repo = payload.repository.full_name;
    const changesUrl = payload.pull_request.diff_url;

    console.log(`Received PR event: ${action} for ${repo}#${prNumber}`);

    // We only care about opened, reopened, or synchronize events for new changes
    if (['opened', 'reopened', 'synchronize'].includes(action)) {
      try {
        await processPullRequest(repo, prNumber, changesUrl);
        res.status(202).send('PR processing initiated.');
      } catch (error) {
        console.error(`Error processing PR ${repo}#${prNumber}:`, error);
        res.status(500).send('Failed to process PR.');
      }
    } else {
      res.status(200).send('Event not relevant for security checks.');
    }
  } else {
    res.status(200).send('Ignoring non-pull_request event.');
  }
});

app.listen(config.port, () => {
  console.log(`SecurityAgentService listening on port ${config.port}`);
});
Enter fullscreen mode Exit fullscreen mode

The real magic happens in processPullRequest. This function fetches the PR diff, identifies Node.js files, extracts relevant code snippets, and then feeds them to an LLM with a highly specific prompt.

// src/services/prProcessor.ts
import axios from 'axios';
import { analyzeCodeForVulnerabilities } from './llmAnalyzer'; // LLM interaction service
import { postCommentToPR } from './githubService'; // GitHub API interaction
import { getFileContentFromDiff } from '../utils/diffParser'; // Custom diff parser

export async function processPullRequest(repo: string, prNumber: number, diffUrl: string) {
  const diffResponse = await axios.get(diffUrl, {
    headers: {
      'Accept': 'application/vnd.github.v3.diff',
      'Authorization': `token ${process.env.GITHUB_TOKEN}`
    }
  });

  const diffContent = diffResponse.data;
  const filesChanged = getFileContentFromDiff(diffContent); // Extracts file paths and changed lines

  let securityFindings: string[] = [];

  for (const file of filesChanged) {
    if (file.filename.endsWith('.js') || file.filename.endsWith('.ts')) {
      const vulnerabilityReport = await analyzeCodeForVulnerabilities(file.patch);
      if (vulnerabilityReport) {
        securityFindings.push(`### Security Findings in \`${file.filename}\`\n\n${vulnerabilityReport}`);
      }
    }
  }

  if (securityFindings.length > 0) {
    await postCommentToPR(repo, prNumber, securityFindings.join('\n---\n'));
  } else {
    await postCommentToPR(repo, prNumber, 'No specific Node.js security vulnerabilities detected by custom agent.');
  }
}

// src/services/llmAnalyzer.ts (simplified for brevity)
import { Anthropic } from '@anthropic-ai/sdk'; // Or OpenAI

const anthropic = new Anthropic({ apiKey: process.env.CLAUDE_API_KEY });

export async function analyzeCodeForVulnerabilities(codePatch: string): Promise<string | null> {
  const prompt = `You are a highly experienced Node.js security auditor. Review the following code patch from a Pull Request. Identify potential SQL injection vectors or untrusted input validation issues. Focus on instances where user input (e.g., from HTTP requests) is used without sanitization or is directly concatenated into database queries or commands. Provide specific line numbers or code snippets where possible, and suggest remediations. If no issues are found, state "No specific issues found."

  \`\`\`diff
  ${codePatch}
  \`\`\`

  Output your findings in a markdown list format, detailing the vulnerability type, location, and suggested fix.`;

  try {
    const msg = await anthropic.messages.create({
      model: "claude-3-opus-20240229", // Or "gpt-4-turbo"
      max_tokens: 1000,
      messages: [{ role: "user", content: prompt }]
    });
    const responseText = msg.content[0].text;
    return responseText.includes("No specific issues found") ? null : responseText;
  } catch (error) {
    console.error("Error calling LLM for security analysis:", error);
    return `_Error during security analysis: ${error.message}_`;
  }
}
Enter fullscreen mode Exit fullscreen mode

This Node.js AI dev tools setup ensures that every relevant PR change gets a dedicated security audit for specific patterns that off-the-shelf tools often miss. It's an extra layer, yes, but 18% fewer vulns in dev is a huge win.

What I Got Wrong First

Honestly, integrating these AI agent systems sometimes feels like building a house of cards. My initial attempts with processing the PR diffs were a mess. I was trying to reconstruct full file contents from partial diffs, which is just asking for trouble with line number mismatches.

The biggest headache was schema validation. I spent two days debugging an issue where my custom agent's API was expecting a specific structure for the changes array, but the webhook payload from an earlier, simpler Repopilot setup didn't quite match. I was constantly hitting this:

Error: [400] Validation Failed: 'body.changes' is required and must be an array of objects matching schema: { filePath: string, patch: string }
Enter fullscreen mode Exit fullscreen mode

Turns out, the Codex SDK project challenges extends to any AI tooling integration if your data models don't align perfectly. My mistake was assuming the diff_url would give me an easily parsable JSON array of changes. It’s a raw diff! So what I did was write a custom diffParser utility (like the getFileContentFromDiff placeholder above) to correctly extract file paths and their respective patches from the raw diff string. This allowed my Node.js service to send the correct payload to the LLM analyzer and eventually to the PR comment system. Always validate your input schemas, especially when dealing with external systems like GitHub webhooks or custom Repopilot extensions. It saves you days.

Optimizing the AI Code Review Agent

The initial runs were slow. Really slow. LLM calls are async, and if you’re iterating through dozens of files on a large PR, you can hit rate limits or just timeout. Here’s how we sped things up for our open source PR automation:

  • Concurrency with Promise.all: Instead of for...of loops for LLM calls, use Promise.all to send multiple requests in parallel (within API rate limits, of course).
  • Targeted Diff Analysis: Don't send the entire file to the LLM. The getFileContentFromDiff function is crucial. It extracts only the changed lines and a small context window around them. This reduces token usage and improves response time dramatically.
  • Caching: For unchanged parts of files, if you have a local copy or can cache previous analyses, avoid re-sending stable code to the LLM. This is more complex but pays off for large repos. We're still iterating on this.
  • Model Choice: While Claude 3 Opus is incredible, it’s not always needed for simpler pattern matching. Sometimes, a faster, cheaper model like Claude 3 Sonnet or even Haiku, or gpt-3.5-turbo, is sufficient for initial triage. Use the more powerful models only for deeper, more complex analysis or when an initial pass flags something suspicious.

I don't get why this isn't the default in more AI code review agent frameworks. The token economy is real, and blindly sending megabytes of code to an LLM is just wasteful.

FAQs

How does Repopilot integrate with custom security rules?

Repopilot itself provides hooks for custom actions. My approach uses its webhook capabilities to trigger a separate, dedicated Node.js service. This service then performs specialized security analysis and reports back via the GitHub/GitLab API, acting as an extension to Repopilot's review.

Can this setup detect zero-day vulnerabilities?

No, this setup focuses on known security patterns (like SQL injection or insecure input handling) that LLMs can identify based on their training and specific prompts. Zero-day vulnerabilities often require deeper, often manual, analysis or runtime behavior monitoring that's beyond the scope of a static PR code review agent.

Is it expensive to run an LLM-powered security agent?

It can be, depending on the LLM model chosen (e.g., Claude 3 Opus is pricier than Haiku) and the volume of PRs. Optimizations like targeted diff analysis, caching, and smart model selection (using cheaper models for initial passes) are essential to manage costs and ensure the Node.js AI dev tools remain economically viable.


Look, AI agent frameworks like Repopilot are powerful, but they’re just tools. The real value comes from how you bend them to solve your specific problems. For us, that meant building out a focused Node.js blueprint to tackle security vulnerabilities head-on, leveraging LLMs in a smart, targeted way. The 18% reduction isn't just a number; it's tangible proof that custom, opinionated tooling, built by devs for devs, actually works. Stop treating security as an afterthought and start integrating it directly into your PR workflow, before it becomes a production incident.

Top comments (0)