eslint-plugin-vibesafe
v0.1.0
Published
Deterministic guardrails for AI-generated TypeScript: comment discipline, strict-tsconfig enforcement, mobile overflow patterns, interactive states, theme-aware colors, and static a11y as one drop-in ESLint plugin, plus a runtime overflow/contrast checker
Downloads
28
Maintainers
Readme
eslint-plugin-vibesafe
Deterministic guardrails for AI-generated TypeScript. One drop-in ESLint plugin that catches the mistakes coding agents actually make: leftover note-taking comments, quietly relaxed compiler strictness, layouts that overflow on phones, interactive elements with no hover or focus states, colors that disappear in dark mode, and inaccessible markup. Plus a runtime checker that proves what static analysis cannot.
Every rule is deterministic. Same input, same findings, no heuristics, no AI.
Install
pnpm add -D eslint-plugin-vibesafeQuick start
For a Next.js app (assumes eslint-config-next is present, which create-next-app sets up):
// eslint.config.mjs
import vibesafe from 'eslint-plugin-vibesafe';
export default [...vibesafe.configs.next];For any other TypeScript project:
import vibesafe from 'eslint-plugin-vibesafe';
export default [...vibesafe.configs.core];Run with warnings blocking, so the comment rules gate CI like everything else:
eslint --max-warnings=0The three layers
| Layer | Catches | When |
|---|---|---|
| ESLint rules (configs.core / configs.next) | comments, type safety, ASCII, overflow patterns, missing interactive states, non-token colors, static a11y | edit time and CI |
| vibesafe/strict-tsconfig | someone quietly turning off strict or noUncheckedIndexedAccess | CI |
| vibesafe-ui (Playwright + axe) | real horizontal overflow at 375px and real contrast failures in light and dark mode | pre-ship |
Custom rules
vibesafe/no-multiline-comments
Any comment block longer than one line gets asked: are these comments necessary, or note-taking left over from implementing or debugging? Delete stale ones; if the code truly needs that much explanation, refactor the code.
The one allowed multi-line form is an interface doc directly above a declaration, one line per label, each label at most once:
// description: parses a class string into variant-aware tokens
// input: raw className string
// output: ClassToken list
// side effects: none
export function tokenize(value: string): ClassToken[] { ... }A label line that wraps to a second line fails the grammar, so the one-line-per-label limit enforces itself. Tool directives (eslint-disable, @ts-expect-error, prettier-ignore, triple-slash references, shebangs) are exempt.
vibesafe/comment-max-length
Comment lines longer than max characters (default 100) are flagged. Options: { max?: number }.
vibesafe/ascii-only
Non-ASCII anywhere in the file: comments, identifiers, and literals in one rule. Em-dash becomes -, smart quotes become ' or ", ellipsis becomes ....
vibesafe/strict-tsconfig
Reads the resolved compiler options through the type-aware parser and errors if strict or noUncheckedIndexedAccess is off. ESLint cannot set compiler flags, but it can refuse to pass while they are relaxed.
vibesafe/no-viewport-width-overflow
Bans w-screen, w-dvw, w-[100vw] and friends. 100vw includes the scrollbar width, which is the classic cause of a page-wide horizontal scrollbar. Use w-full.
vibesafe/no-unresponsive-fixed-width
Fixed widths at or above maxPx (default 360) must carry a responsive prefix: w-[600px] fails, md:w-[600px] passes. Covers Tailwind scale values (w-96 is 384px), arbitrary px and rem values, size-, basis-, min-w-, and inline style widths. max-w-* is exempt because it constrains rather than overflows. Options: { maxPx?: number }.
vibesafe/wide-content-needs-scroll-container
<table> and <pre> must sit inside (or carry) overflow-x-auto or an equivalent scroll container; <pre> may instead wrap with whitespace-pre-wrap. Skips when the class list cannot be statically determined.
vibesafe/no-bare-nowrap
whitespace-nowrap without truncate, an overflow utility, or an ancestor scroll container is a horizontal-overflow bug waiting for long content.
vibesafe/responsive-grid-columns (advisory)
Bare grid-cols-3 and up should usually be grid-cols-1 md:grid-cols-3. This is the one heuristic-adjacent rule, so it ships in a separate configs.advisory pass that nags without failing CI. Options: { minColumns?: number }.
vibesafe/interactive-states
Raw interactive HTML elements (button, a with href, anything with onClick or role="button") must style their states: hover:, active:, focus-visible:, and for buttons disabled:. Capitalized components are exempt since a component library carries its states internally; the rule targets exactly the hand-rolled elements where states get skipped. Skips when the class list cannot be statically determined.
vibesafe/no-unfocusable-outline
outline-none without a focus-visible: replacement removes keyboard users' only affordance. Add focus-visible:ring-2 or similar, or do not remove the outline.
vibesafe/theme-aware-colors
The root cause of "black text on a dark background, can't see anything" is a color-bearing utility that only defines one theme.
mode: 'tokens'(default): raw palette colors (text-gray-900,bg-white,text-[#111]) are banned in favor of semantic tokens (text-foreground,bg-background), which are theme-aware by construction. This is the shadcn/ui model.mode: 'paired': raw colors are allowed only when the element also styles the same property underdark:.
Inline style colors are flagged in both modes since they cannot respond to theme switching at all. Options: { mode?: 'tokens' | 'paired', allow?: string[] }.
Inherited baseline
configs.core also bundles the strict baseline so you do not assemble it per project: typescript-eslint strict plus the type-aware set (floating promises, misused promises, switch exhaustiveness, strict boolean expressions, explicit module boundary types, described-only @ts-expect-error, no non-null assertions), size caps (50 lines per function, 300 per file, complexity 10, depth 4, params 4), hygiene rules, no console.*, no raw process.env outside an env module, no throw new Error() in favor of typed errors, and the full static accessibility set for .tsx files. The accessibility rules come from eslint-plugin-jsx-a11y but are registered under the vibesafe-a11y namespace (vibesafe-a11y/alt-text and so on), so they never collide with eslint-config-next's own jsx-a11y registration. Tests, scripts, and config files get the standard relaxations.
configs.next layers on i18n literal-string enforcement and exemptions for components/ui, Drizzle schemas, and migrations.
Advisory pass
eslint --config eslint.advisory.config.mjs .// eslint.advisory.config.mjs
import vibesafe from 'eslint-plugin-vibesafe';
export default [...vibesafe.configs.advisory];Run it without --max-warnings=0; it nags, it never blocks.
Runtime checker
Static rules catch overflow and contrast risk patterns; the runtime checker proves the outcome. It loads each route at mobile and desktop widths, in light and dark mode, and fails on real horizontal overflow or axe-core contrast violations.
pnpm add -D playwright @axe-core/playwright
pnpm exec playwright install chromium
vibesafe-ui / /pricing /docs --base http://localhost:3000Flags: --viewports 375,1280, --themes light,dark, --dark-class dark (for class-strategy dark mode), --full-axe (fail on all serious/critical axe violations, not just contrast), --timeout 30000.
Philosophy
If a guardrail depends on judgment, an agent will argue its way past it. Every check here is a yes/no question a machine answers the same way every time. The comment rules do not measure comment quality; they measure comment shape, and shape is enough: code that needs paragraphs of explanation needs refactoring, and comments that were scaffolding for a thinking process need deleting.
License
MIT
