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:
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.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.Missing Authentication: Generating CRUD routes is easy. Remembering to apply auth middleware to every single one of them? AI forgets constantly.
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/catchblock that catches a token error but still callsnext(), silently allowing unauthenticated requests through.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
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
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
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'
)
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!
- ⭐ GitHub Repository: github.com/Asyncinnovator/hallint
- 📦 Core Library (npm): @asyncinnovator/hallint
- 📦 CLI (npm): @asyncinnovator/hallint-cli
Top comments (4)
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.
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.
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.
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.