AI Code Quality: Lint Rules Over Documentation

Your Claude.md file telling AI to 'never interpolate SQL' is just a suggestion. Here's how one developer turned good intentions into mechanical guarantees that actually catch mistakes.

How One Developer Built a Lint-Proof AI Code Guard for 10 Production Repos — theAIcatchup

Key Takeaways

  • Documentation-based AI guidance ('always use safeParse') is non-deterministic—custom lint rules and ast-grep patterns enforce it mechanically.
  • Most AI-generated production bugs pass TypeScript and compile cleanly; they're structural antipatterns (N+1 queries, SQL injection) that require domain-specific linting.
  • This developer's three-layer approach (oxlint + ESLint + ast-grep) caught dozens of production-breaking issues that standard tooling missed entirely.

Ninety-one percent of developers now use AI coding assistants. That’s the data point everyone’s celebrating. What nobody talks about? The code that passes TypeScript, runs in dev, and still breaks production—because the AI skipped the .safeParse() and went straight to .parse(), or fired off database mutations without checking the result.

That’s the problem this developer solved. And they did it without switching to a different AI model or waiting for Claude to get “smarter.”

The Problem Nobody Wants to Admit

Here’s the thing about AI-assisted coding: it’s genuinely fast when you already know what you’re doing. But it’s also consistently wrong in ways that are brutally hard to spot. The code compiles. The code runs. The code passes your tests (maybe). But it uses the wrong API flavor, nests async logic four levels deep, or—my personal favorite—treats your database like it’s a toy project.

The original author manages about ten SvelteKit repositories on Cloudflare Workers, all built with Claude Code assistance. After a few close calls, they noticed a pattern: every “always” and “never” statement living in their CLAUDE.md file was functionally optional. The AI reads it. The AI might follow it. The AI also might not. There’s no enforcement. No compiler error. Just… compliance roulette.

“The problem is that if you add guidance to your CLAUDE.md file such as ‘always use safeParse()’ and ‘never interpolate SQL’, those are just suggestions, not constraints. The AI reads them, and might follow them, but also it might not.”

So they did something brilliant: they moved every rule out of prose and into code.

From Wishful Thinking to Mechanical Resistance

In hydraulics, backpressure is resistance applied to a flow to regulate it. In software, it’s making bad patterns structurally impossible rather than merely discouraged. A sign on the pipe that says “please don’t overflow” can be ignored. A pressure valve cannot.

The strategy: migrate every actionable rule out of documentation into something mechanical—a type, a lint rule, a test, a structural check. What stays in CLAUDE.md becomes context and intent. The why, not the what.

They deployed three layers of enforcement:

oxlint does the heavy lifting (~50ms, checks ~200 JS/TS rules). Fast, universal, Rust-based. But it’s dumb about framework patterns.

ESLint + custom Svelte plugin understands .svelte files as a whole. The custom rules are where the magic happens—no-raw-html (sanitization checks), no-binding-leak (platform.env exposure), no-schema-parse (.parse() vs .safeParse()), no-silent-catch (error swallowing).

Then comes ast-grep. This is the newest and most interesting tool in the stack. It uses tree-sitter to match code by structure, not tokens. Rules are declarative YAML. You can describe patterns like: “a database query nested inside an array map.” Neither oxlint nor ESLint can express that. The AI generates it constantly because it looks correct and works on small datasets. It’s a performance catastrophe at scale.

One ast-grep rule catches N+1 queries:

id: n-plus-one-query-map
language: TypeScript
severity: warning
message: Potential N+1 query: database call inside .map(). Use db.batch() or WHERE IN instead.
rule:
  pattern: $ARR.map($$$ARGS)
  has:
    pattern: $DB.prepare($$$SQL)
    stopBy: end

It catches structural antipatterns that look correct on first read but explode under real load.

Why This Matters (and Why It Scares AI Vendors)

Since deploying, these rules flagged:

  • Template literal SQL injection in load functions
  • N+1 patterns where the AI awaited each item individually
  • Unbounded .all() queries that worked in dev (5 rows) and would timeout in production (50,000 rows)
  • Empty catch blocks where D1 errors vanished silently

None produced TypeScript errors. None were caught by oxlint or ESLint. All would have shipped.

This is the uncomfortable truth: your type system doesn’t care about your domain. TypeScript can’t tell the difference between safe and unsafe SQL patterns. It can’t prevent an N+1 query. The linter is the only thing standing between you and a production outage.

Which means AI vendors have a problem. They can’t guarantee code quality because code quality isn’t binary—it’s contextual. What works for a tutorial breaks under load. What follows style guidelines might be structurally unsound. You can’t fix this with a better model or a longer system prompt. You fix it with mechanical enforcement.

The Audit Layer That Catches Churn

Here’s where this gets even more sophisticated. Svelte and Cloudflare ship constantly. SvelteKit has had 50+ releases since 5.0. The AI doesn’t know about features released last week.

So they built a “what’s new” audit—a shell script that fetches their SvelteKit patterns feed (a JSON Feed 1.1 endpoint with grep-friendly search signatures) and the Cloudflare changelog RSS. It filters changelog entries to only the products each repo uses (by reading wrangler.jsonc bindings), then searches source code for legacy patterns.

Output: “repo x has 3 files still using writable() stores—$state class replacement available since Svelte 5.29.”

When the audit discovers features not in the feed, it flags those as gaps. The feed stays current because the audit tells you when it’s behind. The whole thing runs in about 10 seconds across all ten repos. It’s a shell script, not an AI call.

All rules, configs, and workflows live in one .github repo. A sync script distributes to ten consumer repos. One command updates everything. No drift. No decision fatigue.

Why This Is Actually About Human Judgment

Here’s the uncomfortable truth nobody wants to say out loud: better AI models won’t solve this problem. Smarter prompts won’t solve this problem. What solves it is making the implicit explicit and then making the explicit mechanical.

Every rule here came from a mistake they caught. Not a theoretical mistake. Not a “this could theoretically happen.” A real production incident or a close call. That judgment—knowing what matters, knowing what breaks—is irreducibly human. The AI’s job is to generate code fast. Your job is to decide what “safe” actually means for your specific infrastructure, domain, and load profile.

That’s why this approach scales. You’re not arguing with Claude about coding style. You’re not adding another paragraph to your system prompt and hoping it sticks. You’re encoding your actual, hard-won knowledge into structural constraints that can’t be violated.

The irony? The AI’s existence forced this level of rigor. Before Claude Code, maybe nobody bothered. Now? Now you have to be explicit about what’s acceptable. And that’s not a bad thing.

FAQ

Can I use these rules with other AI coding assistants? Yes. These rules are language and tool agnostic—they catch structural mistakes regardless of whether Claude, ChatGPT, or Copilot generated them. The rules are in ESLint, ast-grep, and shell scripts. They work on any code.

What if my linter rules slow down my development? They don’t, according to this setup. oxlint runs in ~50ms. ESLint + custom rules add negligible overhead. ast-grep runs in about 10 seconds across ten repos. The time saved debugging production issues pays for this many times over.

Do I need all three linting tools? No. Start with ESLint + custom rules for framework-specific issues. Add ast-grep only if you’re catching complex structural antipatterns (like N+1 queries) that ESLint can’t express. oxlint is the universal first pass.


🧬 Related Insights

Priya Sundaram
Written by

Hardware and infrastructure reporter. Tracks GPU wars, chip design, and the compute economy.

Frequently asked questions

🧬 Related Insights?
- **Read more:** [GitLab's Package Repository Overhaul: What DevOps Teams Must Do Before September 2026](https://opensourcebeat.com/article/gitlabs-package-repository-overhaul-what-devops-teams-must-do-before-september-2026/) - **Read more:** [How 20+ AI Agents Actually Talk to Each Other—Without Enterprise Bloat](https://opensourcebeat.com/article/how-20-ai-agents-actually-talk-to-each-otherwithout-enterprise-bloat/)

Worth sharing?

Get the best AI stories of the week in your inbox — no noise, no spam.

Originally reported by Dev.to

Stay in the loop

The week's most important stories from theAIcatchup, delivered once a week.