DEV Community

Cover image for Stop Letting AI Write Security Bugs: Introducing "hallint"
Rishu
Rishu

Posted on

Stop Letting AI Write Security Bugs: Introducing "hallint"

We are all using Copilot, Cursor, or ChatGPT to write code faster. They save time, tackle boilerplate, and help us think through complex logic. But there is a massive blind spot that the industry isn't talking about enough: AI coding assistants generate the exact same class of security bugs, over and over again, with total confidence.

Traditional linters like ESLint are fantastic, but they were designed for human-written code. They catch unused variables and missing semicolons. They aren't built to catch the subtle, plausible-looking security holes that LLMs naturally default to.

That is why I built hallint. It is a free, open-source static analysis tool specifically tuned to catch the failure modes of AI code generation before they reach your production environment.


The Problem AI Creates (That ESLint Misses)

AI assistants fail differently than humans do. When you ask an LLM to generate an Express route or a database query, it tends to take the path of least resistance. It writes code that passes casual review, runs perfectly in local development, and creates devastating vulnerabilities once deployed.

Here are a few classic AI failure patterns:

  1. Hardcoded Secrets: AI will happily spit out const API_KEY = "sk-abc123...". It passes linting, but pushing it to a public repo is a disaster.

  2. SQL Injection by Default: AI loves template literals. db.query("SELECT * FROM users WHERE id = ${req.params.id}") looks completely fine at a glance, but it is a textbook SQL injection vector.

  3. Missing Authentication: Generating CRUD routes is easy. Remembering to apply auth middleware to every single one of them? AI forgets constantly.

  4. Auth Masking: This is a particularly insidious AI habit. When asked to add error handling to auth middleware, an LLM will often generate a try/catch block that catches a token error but still calls next(), silently allowing unauthenticated requests through.

  5. Permissive CORS: Setting cors({ origin: '*' }) is the AI's favorite one-liner to "fix" your CORS errors.


What is hallint?

hallint is a TypeScript library and CLI tool that scans your JavaScript, TypeScript, and Python codebases for these specific AI-generated security and quality issues.

Instead of trying to be a general-purpose linter, it uses three detection layers to target AI-specific code smells:

  • Regex pattern matching: A blazing-fast first pass for known bad patterns.
  • AST analysis: Structural checks that understand the code's logic, not just its text.
  • LLM review (Optional): Uses providers like Anthropic, OpenAI, or a local Ollama instance to perform deeper semantic analysis on logic flows.

The Rules Engine

hallint currently ships with 11 targeted rules:

Rule Severity What it catches
hardcoded-secret Critical API keys, tokens, and known prefixes (ghp_, sk-).
sql-injection Critical User input directly interpolated into SQL queries.
unsafe-eval Critical eval() or new Function() using dynamic input.
auth-masking Critical Catch blocks that swallow auth errors, making failures silently pass.
missing-auth-check High Route handlers missing authentication middleware.
xss-innerHTML High Unsanitized strings assigned directly to .innerHTML.
permissive-cors High cors({ origin: '*' }) left in route handlers.
jwt-in-localstorage High JWTs or auth tokens being stored in localStorage.
swallowed-error High Empty or comment-only catch blocks.
http-not-https Medium Hardcoded http:// URLs in fetch/axios requests.
async-no-catch Medium async functions with no error handling at all.

How to Use hallint

You can use hallint as a quick CLI scanner or integrate it deeply into your Node.js tooling.

Running via CLI

You don't even need to install it to try it out. Just point it at your source directory using npx:

npx @asyncinnovator/hallint-cli ./src

Enter fullscreen mode Exit fullscreen mode

Example Output:

hallint scanning ./src...

src/routes/users.ts
  users.ts:4  CRITICAL  [hardcoded-secret]
  Hardcoded secret detected — API key, token, or password in source code
  > const apiKey = "sk-abc123def456ghi789jkl"
  fix: Move to environment variables: process.env.YOUR_SECRET_NAME

  users.ts:9  CRITICAL  [sql-injection]
  Possible SQL injection — user input directly concatenated into a query string
  > const result = await db.query(`SELECT * FROM users WHERE name = ${req.query.name}`)
  fix: Use parameterized queries: db.query('SELECT * FROM users WHERE name = $1', [req.query.name])

Summary: 2 issue(s) in 1 file(s) — 12ms
  2 critical

Enter fullscreen mode Exit fullscreen mode

Using it as a Library

If you are building your own testing pipelines, pre-commit hooks, or editor plugins, you can import the core library directly:

npm install @asyncinnovator/hallint

Enter fullscreen mode Exit fullscreen mode
import { scan, scanSource } from '@asyncinnovator/hallint'

// Scan your file system
const result = await scan({
  files: ['./src/**/*.ts'],
  rules: 'recommended',
  minSeverity: 'high',
})

// Or scan raw strings directly without touching the filesystem
const findings = scanSource(
  `const key = "sk-abc123abc123abc123abc"`,
  'virtual.ts'
)

Enter fullscreen mode Exit fullscreen mode

Dive Deeper: Read the Full Docs on GitHub

If you want to integrate the CLI directly into your CI/CD pipelines (like GitHub Actions) as a hard merge gate, configure ESLint-style inline rule suppressions, set up public route allowlists to reduce noise, or enable the optional LLM-powered plain-English explanations, you will find everything you need in our official documentation.

We want to keep this blog post focused on the core problem, so for advanced configurations, architecture details, and guides on how to write your own custom rules in under 30 lines of code, head over to our repository.

👉 Read the full documentation on GitHub


Get Started

AI is changing how we write code, but we need tools that adapt to the new mistakes we are making. hallint is MIT licensed, community-driven, and highly extensible. If you've seen an AI-specific vulnerability pattern that we don't catch yet, pull requests are always welcome!

Top comments (4)

Collapse
 
mateo_ruiz_6992b1fce47843 profile image
Mateo Ruiz

One thing I've noticed is that AI rarely creates completely new security bugs it tends to reproduce the same insecure patterns consistently. That makes deterministic guardrails (AST analysis, policy checks, secret scanning, auth validation) much more valuable than relying on another LLM to "review" the code afterward.

We've seen similar patterns while building AI-powered applications at IT Path Solutions, especially around generated auth flows and database access. The most reliable approach has been treating AI output as untrusted code that must pass the same CI/CD security gates as human-written code. AI speeds up implementation, but secure defaults still have to come from engineering discipline, not the model.

Collapse
 
asyncinnovator profile image
Rishu

That framing — treat AI output as untrusted code — is exactly it. I keep coming back to that when people ask why not just use another LLM to review it. A rule that says "this string starts with sk- and is 40 characters, reject it" is a much harder guarantee than asking a model to judge whether something looks like a secret.

The auth masking pattern is the one that worries me most honestly. The code looks correct at a glance — there's a try/catch, it's handling errors, what's the problem. The problem is the catch is calling next() which means a thrown auth error silently becomes a passed auth check. That specific mistake is almost always AI. A human writing auth from scratch just doesn't make it.

Collapse
 
raju_dandigam profile image
Raju Dandigam

The useful distinction here is that AI-generated bugs cluster around patterns a style linter was never designed to care about. Catching swallowed errors, hardcoded secrets, or over-trusting input before code review is exactly the kind of gate that belongs in CI next to tests, not in a security retrospective.

The hard part is usually rule credibility over time. False positives decide whether a tool becomes part of the default pipeline or gets bypassed after the first noisy week.

If you can preserve a compact "why this rule fired" receipt per finding, human review also gets much faster. Curious how you are thinking about that tradeoff.

Collapse
 
asyncinnovator profile image
Rishu

Yeah, false positives are what kill tools like this. The first noisy week is basically a make-or-break moment — if engineers start seeing it as "that thing that cries wolf," it gets added to the ignore list and never comes back.

The public route allowlist and inline suppression are my current answer to that, but honestly the real test is running it against enough real codebases to see which rules earn their keep and which ones are too aggressive. That's why I'm building rule fire rate tracking — suppression rate is probably the most honest signal for "this rule is annoying people."

On the receipt idea — that's basically what the LLM explanation layer is trying to be. Each finding can carry a short plain-English note about why that specific snippet triggered it. Less "rule X fired" and more "this API key pattern matches known provider formats." Still early but that's the direction.