Building at Speed with Custom Lint Rules

.avif)
.avif)
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.

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 -yInstall the required dependencies:
npm install - save-dev eslint @typescript-eslint/parser mochaeslint: Required for running the lint rules@typescript-eslint/parser: Enables TypeScript support for your rulesmocha: 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.



