This is a follow-up to I Built a Linter That Catches the Security Bugs AI Assistants Keep Writing. The comments on that post shaped most of what's in this update — thank you.
The first post got a lot of honest feedback. Not just "cool project" — actual technical pushback on the things that weren't right yet. This post covers what we fixed based on that, what's shipped since, and what's coming in v0.2.
Current versions:
-
@asyncinnovator/hallint— v0.1.8 -
@asyncinnovator/hallint-cli— v0.1.7
What the comments got right
Two issues came up repeatedly and they were both valid.
1. async-no-catch was polluting the signal.
Nazar Boyko put it cleanly: the rule breaks the bar of "a human looking at the flagged line agrees it's wrong." Plenty of correct async code has no try/catch because the caller handles it, or because an Express error middleware catches it upstream. Running hallint on a real codebase and getting forty async-no-catch warnings next to one genuine hardcoded key destroys the signal you worked to build.
Fix shipped: async-no-catch is now removed from the recommended ruleset. It still exists and still runs if you opt in with --rules all, but it no longer fires by default. The seven security rules that share the "this line is wrong" property remain in recommended.
2. missing-auth-check had a false positive problem on intentionally public routes.
Dipankar Sarkar and Adam Lewis both flagged this. Health checks, webhooks, public endpoints — they're supposed to have no auth middleware. Flagging them is noise, and noise gets the rule disabled.
Fix shipped: hallint now recognizes three inline suppression markers:
// public
// hallint-public
/* hallint-public */
Add any of these to a route and hallint skips the auth check on that handler. Clean, explicit, no magic inference needed.
app.get('/health', (req, res) => { // hallint-public
res.json({ status: 'ok' })
})
Other fixes in this release cycle
hardcoded-secret now runs dual-pass detection.
The original regex caught generic api_key = "..." patterns. It missed token prefixes that are dead giveaways regardless of variable name. The rule now has a second pass targeting known prefixes: ghp_, sk-, AKIA, xoxb-. A line containing any of these is flagged as critical even if the variable name looks innocent. Comments and env-var assignments are excluded.
sqlInjection message copy was over-claiming.
The original message said "user input flowing into query" but the detection was pattern-based, not data-flow-based. It couldn't actually prove the variable was user input — just that a template literal was used inside a query call. The message now reflects what the rule actually detects: template literal interpolation in a query call. Accurate scope, no false confidence.
Scanner dispatch was refactored.
Previously, the scanner used rule.layer to decide whether to run regex or AST matching. This meant rules had to set layer correctly or they'd be silently skipped. Now rule.match() presence is the dispatch signal — if a rule has a match() function it gets AST treatment, otherwise regex. layer is now metadata only, used for documentation and filtering. This makes rule authoring less error-prone.
asyncNoCatch detection was fragile.
The brace-counter approach used to find async function boundaries broke on nested objects and template literals with braces. Replaced with a stripStrings() heuristic that removes string content before counting — significantly fewer false positives even before the rule was moved out of recommended.
GitHub Actions support
You can now gate CI on hallint in five lines:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npx @asyncinnovator/hallint-cli ./src
hallint exits 1 on critical or high findings, so this blocks merges on real issues. The LLM layer — when it lands — will never trigger exit code 1. Non-deterministic checks don't belong in CI gates.
What's coming in v0.2
A few things were promised in the comments and are actively in progress.
Cross-file router composition tracking.
Right now missing-auth-check is same-file only. If your middleware is registered in app.ts and your routes live in routes/users.ts, hallint doesn't connect them. This is the honest limitation of regex-based analysis — you can't follow imports. v0.2 introduces tree-sitter for AST analysis, which makes cross-file tracking possible. This is the main structural change in v0.2.
LLM layer — opt-in only.
The optional LLM review pass will land in v0.2 as a strict opt-in: --llm ollama or --llm anthropic. It will never run by default and will never contribute to exit code 1. As Kartik noted in the comments, non-determinism in a CI gate is a dealbreaker. The LLM layer surfaces things the regex and AST layers miss — semantic issues, logic flaws, context-blind patterns — but those findings go in a separate output section, not in the main results that block a merge.
New rules under consideration for v0.2–v0.3:
-
silent-error-swallow— bare catch blocks that swallow errors without logging or rethrowing. The pattern Edu Peralta described: looks handled, isn't. -
jwt-in-localstorage— storing tokens where XSS can reach them
- hallucinated-dependency — imports of packages that don't exist in package.json (a real AI-specific failure mode)
Contributing
Each rule is a single file, around 30 lines, with a bad.ts and good.ts fixture. If you've seen a pattern AI assistants keep producing that hallint doesn't catch yet, the path from "I noticed this" to "I shipped a fix" is short.
Issues labeled good first issue are pre-scoped and ready to pick up.
GitHub: github.com/Asyncinnovator/hallint
npx @asyncinnovator/hallint-cli ./src
MIT licensed. Free for personal and commercial use.
Top comments (0)