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.
After the first post went up, I got a comment that stuck with me. Nazar Boyko wrote:
"Seven of your eight rules are 'this is a vulnerability.'
async-no-catchis the odd one out... The first time someone runs hallint on a real codebase and gets forty of those next to one genuine hardcoded key, the signal you worked for is gone."
He was right. And fixing it taught me something about what makes a security linter trustworthy.
The Signal Problem
The whole point of hallint is to give you a reliable signal: this line is wrong. That's what makes it useful as a CI gate. When a finding fires, a developer — or an AI agent — should be able to look at the flagged line and agree without needing more context.
async-no-catch broke that bar. Plenty of correct async code has no try/catch because:
- The caller handles the rejection
- An Express error middleware catches it upstream
- Throwing is the intended behavior
Medium severity didn't save it. The moment you get forty
async-no-catchwarnings alongside one genuine hardcoded API key, you stop trusting the output. You turn the rule off. And now you've lost the one thing a security linter has to protect: the cost of ignoring a finding is zero.
The fix: async-no-catch is now removed from recommended. It still exists — --rules all opts you in — but it no longer ships alongside the seven security rules by default. recommended stays clean and gateable.
This is the difference between a linter that's interesting and one that's actually wired into CI.
The False Positive Problem on missing-auth-check
Dipankar Sarkar and Adam Lewis both flagged the same issue independently: health checks, webhooks, and public endpoints are supposed to have no auth middleware. Flagging them is noise. And noise gets rules disabled.
The fix needed to be explicit, not inferred. hallint now recognizes three inline suppression markers:
// public
app.get('/health', (req, res) => res.json({ status: 'ok' }))
// hallint-public
router.get('/metrics', (req, res) => res.json(metrics))
router.post('/webhook', /* hallint-public */ async (req, res) => {
res.sendStatus(204)
})
Any of these tells hallint the route is intentionally public and skips the auth check. The marker can go on the line above or inline on the same line. No config file needed.
Other Fixes Shipped Since v0.1.0
hardcoded-secret — dual-pass detection
The original regex caught assignment-style secrets (api_key = "abc123"). It missed token prefixes that are dead giveaways regardless of variable name. The rule now runs a second pass targeting known provider-issued formats: ghp_, ghs_, sk-, AKIA, xoxb-, xoxp-, AIza, ya29.. A line containing any of these is flagged as critical even if the variable name looks innocent. Comment lines and process.env. reads are excluded.
sql-injection — honest message copy
The original message claimed "user input flowing into query." The detection was pattern-based — it couldn't actually prove data flow, just that a template literal appeared inside a query call. The message now says what the rule actually detects: template literal interpolation in a query string. Accurate scope, no false confidence.
Scanner dispatch — behavior now follows code, not metadata
Previously the scanner used rule.layer to decide whether to run regex or match(). A rule with layer: "ast" but no match() function would be silently skipped. Now rule.match() presence is the dispatch signal — if a rule defines match() it gets AST treatment, otherwise regex. layer is metadata only. Rule authoring is now harder to break.
async-no-catch brace counting — stripStrings() heuristic
Before being moved out of recommended, the rule's brace counter was made more robust. Template literals and inline strings were throwing off the depth counter, causing false positives. A stripStrings() pass now removes string content before counting { / }, significantly reducing noise.
Current Versions
| Package | Version |
|---|---|
@asyncinnovator/hallint |
v0.1.8 |
@asyncinnovator/hallint-cli |
v0.1.7 |
Install:
npm install @asyncinnovator/hallint
Or run without installing:
npx @asyncinnovator/hallint-cli ./src
CI Integration
hallint exits 1 on critical or high findings. Drop it into GitHub Actions in four lines:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npx @asyncinnovator/hallint-cli ./src
This blocks merges on real findings. The LLM layer — when it ships — will never contribute to exit code 1. Non-determinism doesn't belong in a CI gate.
What's Coming in v0.2
Cross-file router composition tracking
Right now missing-auth-check is same-file only. Middleware registered in app.ts isn't connected to routes defined in routes/users.ts. This is the honest limitation of regex analysis — you can't follow imports. v0.2 introduces tree-sitter, which makes cross-file tracking possible. This is the main structural change.
LLM layer — opt-in only
The optional LLM review pass ships in v0.2 as a strict opt-in (--llm ollama or --llm anthropic). It surfaces semantic issues and context-blind patterns the regex and AST layers miss, but those findings go in a separate output section and never 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. Looks handled. Isn't. -
jwt-in-localstorage— storing tokens where XSS can reach them. -
hallucinated-dependency— imports of packages that don't exist inpackage.json(a genuine AI-specific failure mode)
Contributing
Each rule is a single file, ~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, the path from "I noticed this" to "I shipped a fix" is short. Issues labeled good first issue are pre-scoped and ready.
GitHub: github.com/Asyncinnovator/hallint
npm: @asyncinnovator/hallint · @asyncinnovator/hallint-cli
MIT licensed. Free for personal and commercial use.
Top comments (1)
I like the follow-up format here because security tooling earns trust through fixes, not just launch claims.
For a linter aimed at AI-written bugs, the changelog matters: false positives, missed patterns, performance, and rule clarity all decide whether developers keep it in the loop after the first interesting demo.