Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Always use ESLint with @convex-dev/eslint-plugin to catch Convex-specific issues and enforce best practices
.claude/skills/kunanonj-cursor-plugin-convex-rule-use-eslint-always/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 80% | 30 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 91% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 160% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 110% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 76% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 59% | 0% |
Every Convex project should use ESLint with the official @convex-dev/eslint-plugin to catch common mistakes and enforce best practices.
ESLint catches issues that TypeScript can't:
await on promises (floating promises).filter() instead of indexes.collect() without paginationWithout ESLint, you'll:
With ESLint, you'll:
bashnpm install --save-dev @convex-dev/eslint-plugin
Modern (Flat Config) - Recommended:
javascript// eslint.config.mjs import convexPlugin from "@convex-dev/eslint-plugin"; export default [ // Apply to all files ...convexPlugin.configs.recommended, // Your custom rules { rules: { // Add your overrides here }, }, ];
Legacy (.eslintrc.js):
javascript// .eslintrc.js module.exports = { extends: ["plugin:@convex-dev/recommended"], rules: { // Your custom rules }, };
json{ "scripts": { "lint": "eslint .", "lint:fix": "eslint . --fix", "typecheck": "tsc --noEmit" } }
bash# Check for issues npm run lint # Auto-fix issues npm run lint:fix
The @convex-dev/eslint-plugin includes these critical rules:
no-floating-promises)Catches:
typescriptexport const createTask = mutation({ handler: async (ctx, args) => { ctx.db.insert("tasks", args); // ❌ Missing await! }, });
Fixes to:
typescriptexport const createTask = mutation({ handler: async (ctx, args) => { await ctx.db.insert("tasks", args); // ✅ Awaited }, });
require-argument-validators)Catches:
typescriptexport const getTask = query({ // ❌ Missing args validator handler: async (ctx, args) => { return await ctx.db.get(args.taskId); }, });
Fixes to:
typescriptexport const getTask = query({ args: { taskId: v.id("tasks") }, // ✅ Validator added handler: async (ctx, args) => { return await ctx.db.get(args.taskId); }, });
explicit-table-ids)Catches:
typescriptconst task = await ctx.db.get(taskId); // ❌ Missing table name
Fixes to:
typescriptconst task = await ctx.db.get("tasks", taskId); // ✅ Table name added
Note: Convex now requires table names in ctx.db.get(), patch(), replace(), and delete().
no-query-collect)Catches:
typescriptconst allTasks = await ctx.db.query("tasks").collect(); // ⚠️ Potentially huge!
Suggests:
typescript// Use pagination for large datasets const results = await ctx.db.query("tasks").paginate({ cursor: null, limit: 100, });
prefer-indexes)Catches:
typescriptconst user = await ctx.db .query("users") .filter(q => q.eq(q.field("email"), email)); // ❌ Slow!
Suggests:
typescriptconst user = await ctx.db .query("users") .withIndex("by_email", q => q.eq("email", email)); // ✅ Fast!
Add these TypeScript ESLint rules for Convex:
javascript// eslint.config.mjs export default [ ...convexPlugin.configs.recommended, { rules: { // Floating promises (critical!) "@typescript-eslint/no-floating-promises": "error", // Misused promises "@typescript-eslint/no-misused-promises": "error", // Require await in async functions "require-await": "error", // No console.log in production "no-console": ["warn", { allow: ["warn", "error"] }], // Enforce return types "@typescript-eslint/explicit-function-return-type": ["warn", { allowExpressions: true, }], // No any (encourage proper typing) "@typescript-eslint/no-explicit-any": "warn", // No unused vars "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", }], }, }, ];
Enable strict mode in tsconfig.json:
json{ "compilerOptions": { "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "forceConsistentCasingInFileNames": true } }
Use Husky + lint-staged to lint before commits:
bashnpm install --save-dev husky lint-staged npx husky init
javascript// .husky/pre-commit npm run lint npm run typecheck
Or with lint-staged for faster commits:
json// package.json { "lint-staged": { "*.{ts,tsx,js,jsx}": [ "eslint --fix", "prettier --write" ] } }
Add to your CI pipeline:
yaml# .github/workflows/ci.yml name: CI on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npm run lint - run: npm run typecheck
Install ESLint extension:
json// .vscode/extensions.json { "recommendations": [ "dbaeumer.vscode-eslint" ] }
Enable auto-fix on save:
json// .vscode/settings.json { "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact" ] }
Same settings as VS Code (uses VS Code engine).
typescript// ❌ Error export const create = mutation({ handler: async (ctx, args) => { ctx.db.insert("tasks", args); }, }); // ✅ Fix export const create = mutation({ handler: async (ctx, args) => { await ctx.db.insert("tasks", args); }, });
typescript// ❌ Error export const get = query({ handler: async (ctx, args) => { return await ctx.db.get(args.id); }, }); // ✅ Fix export const get = query({ args: { id: v.id("tasks") }, handler: async (ctx, args) => { return await ctx.db.get(args.id); }, });
typescript// ❌ Error (old style) await ctx.db.get(taskId); // ✅ Fix (new style) await ctx.db.get("tasks", taskId);
typescript// ❌ Error const all = await ctx.db.query("tasks").collect(); // ✅ Fix const results = await ctx.db.query("tasks").paginate({ cursor: null, limit: 100, });
Sometimes you need to disable a rule:
typescript// Disable for one line // eslint-disable-next-line @convex-dev/no-query-collect const all = await ctx.db.query("tasks").collect(); // Disable for whole file /* eslint-disable @convex-dev/no-query-collect */ // Disable in config for specific files export default [ { files: ["convex/migrations/**"], rules: { "@convex-dev/no-query-collect": "off", }, }, ];
⚠️ Warning: Only disable rules when you have a good reason. Most Convex ESLint rules exist to prevent real bugs!
bash# Make sure plugin is installed npm ls @convex-dev/eslint-plugin # Reinstall if needed npm install --save-dev @convex-dev/eslint-plugin
Make sure your ESLint config includes the convex directory:
javascriptexport default [ { files: ["**/*.ts", "**/*.js"], // Rules apply to all files including convex/ }, ];
Make sure @typescript-eslint/parser is installed:
bashnpm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
bash# Install everything npm install --save-dev \ eslint \ @convex-dev/eslint-plugin \ @typescript-eslint/parser \ @typescript-eslint/eslint-plugin \ prettier \ eslint-config-prettier
javascript// eslint.config.mjs import convexPlugin from "@convex-dev/eslint-plugin"; import tseslint from "@typescript-eslint/eslint-plugin"; import tsparser from "@typescript-eslint/parser"; import prettier from "eslint-config-prettier"; export default [ // Convex recommended rules ...convexPlugin.configs.recommended, // TypeScript rules { files: ["**/*.ts", "**/*.tsx"], languageOptions: { parser: tsparser, parserOptions: { project: "./tsconfig.json", }, }, plugins: { "@typescript-eslint": tseslint, }, rules: { "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", }], }, }, // Prettier (must be last) prettier, ];
json// package.json { "scripts": { "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write .", "typecheck": "tsc --noEmit", "check": "npm run lint && npm run typecheck" } }
Without ESLint:
typescript// Ships to production with bugs! export const update = mutation({ handler: async (ctx, args) => { ctx.db.patch(args.id, args.data); // Forgot await console.log("Updated!"); // Logs before update! } });
With ESLint:
bash$ npm run lint convex/tasks.ts 3:5 error Promises must be awaited @typescript-eslint/no-floating-promises ✖ 1 problem (1 error, 0 warnings)
You catch the bug before it reaches production!
@convex-dev/eslint-pluginnpm run lint regularlyRemember: ESLint is not optional for production Convex apps. It catches bugs that will slip past TypeScript!
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-03 | fail→pass | 15,606 | 10,087 | -35% | 1 | 1 | 0% | 2,763 | 5,268 | +91% | 0 | 0 | — |
case-02 | fail→fail | 9,412 | 10,360 | +10% | 1 | 1 | 0% | 1,737 | 5,158 | +197% | 0 | 0 | — |
case-01 | fail→pass | 9,957 | 7,518 | -24% | 1 | 1 | 0% | 1,817 | 4,729 | +160% | 0 | 0 | — |
case-04 | pass→pass | 8,210 | 8,049 | -2% | 1 | 1 | 0% | 1,660 | 4,781 | +188% | 0 | 0 | — |
case-05 | pass→pass | 12,956 | 16,867 | +30% | 1 | 1 | 0% | 2,543 | 6,624 | +160% | 0 | 0 | — |
case-06 | pass→pass | 7,187 | 6,118 | -15% | 1 | 1 | 0% | 1,216 | 4,325 | +256% | 0 | 0 | — |
case-07 | fail→pass | 13,766 | 9,058 | -34% | 1 | 1 | 0% | 2,398 | 5,034 | +110% | 0 | 0 | — |
case-08 | pass→pass | 5,319 | 4,579 | -14% | 1 | 1 | 0% | 929 | 4,129 | +344% | 0 | 0 | — |
case-09 | fail→pass | 13,096 | 3,669 | -72% | 1 | 1 | 0% | 2,265 | 3,977 | +76% | 0 | 0 | — |
case-10 | fail→pass | 12,602 | 2,898 | -77% | 1 | 1 | 0% | 2,418 | 3,845 | +59% | 0 | 0 | — |
case-11 | fail→pass | 15,304 | 3,134 | -80% | 1 | 1 | 0% | 2,553 | 3,808 | +49% | 0 | 0 | — |
case-12 | fail→pass | 12,940 | 3,553 | -73% | 1 | 1 | 0% | 2,158 | 3,939 | +83% | 0 | 0 | — |
case-13 | fail→pass | 8,162 | 4,734 | -42% | 1 | 1 | 0% | 1,412 | 4,087 | +189% | 0 | 0 | — |
case-14 | fail→pass | 10,078 | 5,510 | -45% | 1 | 1 | 0% | 1,791 | 4,382 | +145% | 0 | 0 | — |
case-15 | pass→pass | 11,390 | 7,170 | -37% | 1 | 1 | 0% | 2,015 | 4,539 | +125% | 0 | 0 | — |
case-16 | fail→pass | 3,282 | 2,541 | -23% | 1 | 1 | 0% | 591 | 3,772 | +538% | 0 | 0 | — |
case-17 | pass→pass | 11,264 | 8,963 | -20% | 1 | 1 | 0% | 2,007 | 5,116 | +155% | 0 | 0 | — |
case-18 | pass→pass | 11,674 | 3,701 | -68% | 1 | 1 | 0% | 1,997 | 3,937 | +97% | 0 | 0 | — |
case-19 | fail→fail | 7,905 | 6,431 | -19% | 1 | 1 | 0% | 1,570 | 4,466 | +184% | 0 | 0 | — |
case-20 | pass→pass | 10,038 | 7,164 | -29% | 1 | 1 | 0% | 1,832 | 4,566 | +149% | 0 | 0 | — |
case-21 | pass→pass | 8,755 | 3,212 | -63% | 1 | 1 | 0% | 1,584 | 3,764 | +138% | 0 | 0 | — |
case-22 | pass→pass | 4,697 | 2,514 | -46% | 1 | 1 | 0% | 784 | 3,673 | +368% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +45 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.