Axel Vincent
← Back

Lint Rules Are Codebase Context Engineering

March 2, 2026 · 8 min read

I've shipped this pattern across every company I've worked at and every project I orchestrate for my clients. It works every time. Here's why.


The problem with prompt-based instructions#

CLAUDE.md, .cursorrules, copilot-instructions.md, agent.md. Every AI coding tool has some version of "write your conventions here and the model will follow them."

You write: "Always validate API route inputs with Zod before processing the request."

The AI reads it. Nods. Then writes this anyway:

typescript
// routes/users.ts
app.post('/users', async (req, res) => {
  const { email, role } = req.body // raw, unvalidated
  await UserService.create({ email, role })
})

You add another instruction: "Never write database queries outside of the query layer."

Same result:

typescript
// user.service.ts
const user = await db.selectFrom('user').where('id', '=', id).selectAll().executeTakeFirst() // wrong layer

Why? Your instructions compete with millions of data points from public codebases that do exactly this. They lose. Every time.

A lint rule doesn't compete. It enforces.


Context window economics#

Every line in your instruction file eats tokens from the agent's context window. Every turn. Whether those instructions are relevant to the current task or not.

That context window is the most valuable resource your agent has. It needs to hold the task, the relevant files, the conversation history. The things that actually drive output quality. Your 30 lines of architectural rules loaded into context on every turn? They're competing with the code the agent is trying to reason about.

Lint rules cost zero context. They live outside the agent entirely. The only time they touch the context window is when they fire, and when they do, the error message is surgical: targeted to the exact violation, actionable, self-contained. Not a wall of rules the agent has to mentally filter on every turn.

Sub-agents benefit even more. When your coding agent spawns sub-tasks, those sub-agents inherit the lint configuration from the project automatically. No context forwarding. No dilution. The architectural constraints travel with the toolchain, not the prompt.

This is harness engineering. You're not writing better instructions. You're designing the environment so the agent self-corrects through its own feedback loop. The context window stays focused on what matters. The lint infrastructure handles everything else.


The real cost of skipping guardrails#

Ungoverned AI isn't just inconsistent. It's a technical debt accelerator.

Every task the AI completes without constraints makes the next task harder. Inconsistent folder structures. Raw req.body calls scattered across 40 routes. Queries leaking into controllers. Each violation becomes a new pattern the AI replicates, because it reads your codebase as a signal for the next completion.

The math is brutal: the more you let it drift, the more it anchors to the drift.

A homogeneous codebase inverts this dynamic. When every file follows the same structure, every route validates the same way, and every query lives in the same place. The AI pattern-matches against your conventions instead of fighting them. Its output gets faster and cleaner with each task.

This works for humans too. A new engineer joins the team, reads one service file, and knows how every service file works. The lint errors they hit on day one teach them the conventions faster than any onboarding doc. The codebase becomes self-teaching, for humans and agents alike.

Lint rules are what make that homogeneity enforceable. Not aspirational.


End-to-end: what this looks like in practice#

Here's the full loop, from AI output to clean diff.

Step 1. The AI generates a new route:

typescript
router.post('/users', async (req, res) => {
  const user = await db.selectFrom('user').where('id', '=', req.body.id).selectAll().executeTakeFirst()
  res.json(user)
})

Step 2. ESLint fires two errors simultaneously:

bash
ESLint: 'POST /users' is missing the validateRequest middleware.
ESLint: Direct database queries are not allowed here. Create a dedicated query file.

Step 3. The AI reads both error messages and self-corrects, creates a dedicated query file, adds a contract.ts with the appropriate schemas, wires validateRequest into the route. No human prompt needed.

Step 4. ESLint runs again. Zero errors. The diff is mergeable.

No review comment. No second prompt. No human in the loop.


Let the agent clean the existing mess#

Once a rule is wired into CI, you don't have to manually fix existing violations. You point a coding agent at the lint output and tell it to fix everything.

bash
Run eslint --format json. For every error, fix the violation to match
the expected pattern described in the error message. Do not change any
business logic. Only fix what the linter flags. Re-run eslint after each
fix to confirm the error is resolved before moving to the next.

This works because lint errors are a perfect feedback loop for an agent. They're deterministic, scoped, and self-describing. The agent doesn't need to understand your architecture. It just needs to follow the constraint until the output is green.

You introduce a new rule on a codebase with 60 violations. You spin up Claude Code with the instruction above. 60 errors. Less than an hour. Zero left. Every file fixed, passing CI, without a human touching a single line.

What would have taken a senior engineer a full day of careful, tedious work takes an agent less than an hour. With higher consistency. And a CI gate that proves it.

This changes the economics of introducing new rules. The usual objection to adding a strict rule to an existing codebase is the backlog it creates: hundreds of violations, no time to fix them, rule sits on warn forever, nobody respects it.

With an agent, that backlog is not a reason to delay. It's just a task. Write the rule. Run the agent. Merge the cleanup PR. Flip to error. Done.


The objection I always hear: "Writing lint rules takes time."#

Here's the irony: the AI writes them.

bash
You are an ESLint rule engineer. I'll describe a coding constraint I want
to enforce in my codebase. Your job is to:
 
1. Write a complete, working ESLint custom rule in CommonJS format
   with a proper meta block and module.exports
2. Include the rule logic and a clear, actionable error message
3. Add an auto-fixer when the correction is unambiguous
4. Add a RuleTester file with valid and invalid cases
 
Constraint to enforce: [describe it in plain English]

Describe your constraint in one sentence. Get a working rule back in one turn. You'll occasionally need to iterate on edge cases, monorepo path handling, dynamic imports, middleware composition patterns. But the structure and logic are done. The hard part takes 30 seconds. The rest is tuning.

The tool that governs the AI is built by the AI. The only effort left is yours: notice the problem once.


Lint rules are living documentation#

Every rule you write is versioned, executable documentation. It lives in your repo. It evolves with your architecture. It can't go stale, because a stale rule breaks the build and forces an update.

Compare that to a wiki page, a README section, or an instruction file that quietly drifts from reality the week after someone writes it.

When a new engineer or agent asks, "How do we structure queries here?" the answer isn't buried in a doc. It's in the rule that fires the moment they get it wrong.


How to start#

Audit your last 20 code reviews. For every comment, ask: is this a convention violation or a logic issue?

Convention violations are lint rule candidates. Logic issues need human judgment.

Every convention comment that appears twice or more is a rule worth writing. Paste it into the prompt above. Wire it into CI. That comment never appears in a review again.

Start on warn. Measure frequency. Flip to error. Ship it.


The real shift#

Prompt-based instructions aren't useless. CLAUDE.md, .cursorrules, copilot-instructions.md, agent.md. They're the right place for tone, style, workflow preferences, and things that can't be statically analyzed. Use them for that.

But convention enforcement? Architecture constraints? That's not a preference. That's a contract.

Your instruction file tells the AI what you prefer.

Lint rules tell it what's non-negotiable.

One gets ignored under pressure. The other doesn't compile.

One burns context tokens every turn. The other costs nothing until it matters.

The barrier to writing them is gone. The AI writes the rules. The agent handles the cleanup. You're left with one job: notice the problem once.

The rule handles it forever.


If you have read all this, let's connect! Feel free to follow and send me a dm: https://x.com/AxelVincent_