Back

Building at Speed with Custom Lint Rules

Nitzan Kletter
Nitzan Kletter
February 16, 2026
Engineering
Building at Speed with Custom Lint RulesBright curved horizon of a planet glowing against the dark backdrop of space.Bright curved horizon of a planet glowing against the dark backdrop of space.

At Daylight Security, we believe the best engineering happens when you can move fast, but only within guardrails that keep you aligned, consistent, and secure.

This is the first in a series of articles titled “pragmatic AI-native engineering”, where we share how we put these principles into practice in our day-to-day development.

AI writes its best code when the boundaries are clear. Custom lint rules transform our collective knowledge - the patterns we value, the pitfalls we avoid - into an automated system that runs everywhere, for everyone.

Writing custom rules are a perfect example of pragmatic AI-native engineering in action: given explicit boundaries, auto-fix guidance, and testable expectations across the repository, AI tools can provide the best results.
This means less ambiguity, fewer regressions, reduced review churn, and far more consistency between human and AI-generated code.

Here’s how we did it, and how you can too.

Press enter or click to view image in full size

Setting Up Custom ESLint Rules

Step 1: Create the Plugin Structure

Create an eslint-rules/ directory at your project root:

mkdir eslint-rules
cd eslint-rules
npm init -y

Install the required dependencies:

npm install - save-dev eslint @typescript-eslint/parser mocha
  • eslint: Required for running the lint rules
  • @typescript-eslint/parser: Enables TypeScript support for your rules
  • mocha: For writing tests (alternatives: jest/vitest)

Step 2: Create the Plugin Entry Point

Create eslint-rules/index.js:

/**
 * @fileoverview Custom ESLint rules for the project
 */

const myCustomRule = require("./rules/my-custom-rule.js");

module.exports = {
  rules: {
    "my-custom-rule": myCustomRule,
  },
  configs: {
    recommended: {
      plugins: ["eslint-rules"],
      rules: {
        "eslint-rules/my-custom-rule": "error",
      },
    },
  },
};

Step 3: Write Your First Rule

Create eslint-rules/rules/ directory and add your rules. Here’s an example snippet representing a rule from our codebase:

// eslint-rules/rules/require-page-metadata.js
module.exports = {
  meta: {
    type: "problem",
    docs: {
      description: "Enforce metadata exports in Next.js pages",
      category: "Best Practices",
      recommended: true,
    },
    messages: {
      missingMetadata: "Next.js page must export metadata",
      invalidTitle: "Title must start with 'Daylight - '",
    },
  },
  create(context) {
    return {
      Program(node) {
        const filename = context.filename;
        if (!filename.endsWith("page.tsx")) return;
        // Check for metadata export...
      },
    };
  },
};

Step 4: Install and Configure

In your main package.json, add the local plugin:

{  "dependencies": {    "eslint-plugin-eslint-rules": "file:./eslint-rules"  }}

Then in eslint.config.js:

import eslintRules from "eslint-plugin-eslint-rules";

export default [
  ...compat.extends("plugin:eslint-rules/recommended"),
  {
    plugins: {
      "eslint-rules": eslintRules,
    },
  },
];

Step 5: Write Tests for Your Rules

Testing your custom ESLint rules ensures they work as expected and prevents regressions. The good news? LLMs are excellent at writing these tests.

Create a test file in eslint-rules/tests/:

// eslint-rules/tests/my-custom-rule.test.js
const { RuleTester } = require("eslint");
const rule = require("../rules/my-custom-rule");

const ruleTester = new RuleTester({
  parserOptions: { ecmaVersion: 2015, sourceType: "module" },
});

ruleTester.run("my-custom-rule", rule, {
  valid: [
    // Test cases that should pass
    { code: 'import { useAuth } from "~/context/SessionDetailsProvider";' },
  ],
  invalid: [
    // Test cases that should fail
    {
      code: 'import { useAuth } from "third-party-sdk";',
      errors: [{ messageId: "noDirectImport" }],
    },
  ],
});

When to Use Custom ESLint Rules

Custom rules shine when you need to enforce patterns that can’t be captured by off-the-shelf linting. Here are real examples from our codebase:

1. Enforcement: Preventing Direct Third-Party Imports

Problem: Direct imports from auth provider libraries scattered across the codebase can make migrations painful.

Solution: no-direct-auth-provider-imports rule

// ❌ This fails the lint check
import { useAuth } from "third-party-sdk";

// ✅ This passes
import { useAuth } from "~/context/SessionDetailsProvider";

Why it matters: When we need to swap auth providers, we only update one wrapper instead of hunting down dozens of direct imports. Same rule of thumb can be applied to external icon libraries, analytics providers, or anything else that could require flexibility

2. Custom Business Logic: Enforcing Tenant Context

Problem: Next.js’s useRouter doesn’t know about our multi-tenant architecture.

Solution: no-next-router rule

// ❌ This fails
import { useRouter } from "next/navigation";

// ✅ This passes
import { useTenantRouter } from "~/hooks/use-tenant-router";

Why it matters: Every route automatically includes tenant context, preventing bugs where tenant ID is missing from URLs.

3. Intentional Observability: No Console Statements

Problem: console.error statements scattered throughout the codebase (a pattern that LLMs use generously in their generated try/catch blocks) can make it impossible to track errors efficiently.

Solution: no-console-statements rule that guides to use our custom utility function.

// ❌ This fails in client code
console.error("OAuth token refresh failed");

// ✅ This passes
recordError(new Error("OAuth token refresh failed"), { provider, userId });

Why it matters: Forces deliberate observability decisions, allowing us to distinguish known from unknown errors, create dashboards, set up alerts, and maintain centralized error tracking across our entire application.

The Bottom Line

Writing custom lint rules is an effective way to turn implicit knowledge into explicit constraints, which is equally great for humans and LLMs.

Interested in building AI-native security engineering? We’re hiring at Daylight Security.

Table of content
form submission image form submission image

Ready to escape the dark and elevate your security?

button decoration
Get a demo
moutain illustration